Managing State In React Using Unstated Next

Discover how unstated-next provides a minimalist approach to state management in React, offering ~200 bytes of code with fine-grained reactivity for modern web applications.

State management remains one of the most critical decisions when building React applications. While solutions like Redux have dominated the ecosystem for years, the React landscape has evolved significantly with the introduction of hooks. Modern web development with Next.js demands approaches that balance simplicity with performance, and unstated-next represents a minimalist philosophy that leverages React's built-in capabilities rather than fighting against them.

The philosophy behind unstated-next centers on a provocative premise: what if you didn't need a state management library at all? Created by Jamie Kyle, unstated-next weighs in at approximately 200 bytes minified and gzipped, making it virtually negligible in terms of bundle impact. This tiny footprint is achieved not through clever compression tricks but through a design that trusts React's own state management primitives. The library provides just enough structure to solve common state-sharing problems while remaining fundamentally simple.

For developers building React applications that prioritize performance and maintainability, unstated-next offers a compelling middle path between the simplicity of React's built-in state and the complexity of full-featured state management libraries. When combined with proper testing practices, your state management becomes both reliable and performant.

Why Unstated-next for Modern React?

Key benefits that make unstated-next a compelling choice for performance-conscious developers

Minimal Bundle Size

At approximately 200 bytes minified and gzipped, unstated-next adds virtually no weight to your application bundle.

Familiar React Patterns

Built entirely on React hooks, the API requires no new paradigms--use your existing React knowledge.

Fine-Grained Reactivity

Subscription model prevents unnecessary re-renders by only updating components that use specific state values.

Zero Boilerplate

No action types, reducers, or complex setup--just containers and hooks for immediate productivity.

Core API Fundamentals

The unstated-next API consists of just two primary functions: createContainer and useContainer. This minimalism is a deliberate design choice that keeps the library easy to understand while remaining flexible enough for real-world use cases.

Creating State Containers

A container is created by calling createContainer with an initializer function. This initializer can return either a simple value or an object containing state and methods for modifying that state.

import { createContainer } from 'unstated-next';

function useCounter(initialValue = 0) {
 const [count, setCount] = useState(initialValue);
 return { count, increment: () => setCount(c => c + 1) };
}

const CounterContainer = createContainer(useCounter);

This pattern allows you to define state logic in a custom hook while gaining the ability to share that state across components. The custom hook can use any React hooks internally, making containers as flexible as the hooks they're built upon.

The container model in unstated-next encourages a separation of concerns that mirrors component design: just as you compose UI from small, focused components, you compose state from small, focused containers. Each container owns a coherent slice of state and the logic for modifying it, keeping concerns organized and testable.

Consuming Containers in Components
1import { useContainer } from 'unstated-next';2import { CounterContainer } from './containers';3 4function CounterDisplay() {5 const { count, increment } = useContainer(CounterContainer);6 7 return (8 <div>9 <p>Count: {count}</p>10 <button onClick={increment}>Increment</button>11 </div>12 );13}

Consuming Containers in Components

Components access container state through the useContainer hook. This hook returns whatever value the container's initializer returns, whether that's a single value or an object with multiple properties. Components can read from this state and call methods to update it, just as they would with any React hook.

The beauty of this approach lies in its explicitness. When a component uses useContainer, it's clear which state it's depending on. This transparency makes debugging easier and helps developers reason about component dependencies.

Organizing Multiple Containers

As applications grow, you'll likely need multiple containers for different domains of state. The recommended approach is to create separate containers for each logical concern, then compose them together as needed. This mirrors how you organize custom hooks in React--group related state and logic together.

Performance Considerations

Performance is where unstated-next truly differentiates itself from alternatives. The subscription model provides fine-grained reactivity that prevents unnecessary re-renders, while the minimal bundle size ensures fast initial load times.

Subscription vs. Context API

React's Context API is powerful but comes with a significant caveat: any change to a context value causes all consumers of that context to re-render, regardless of whether they use the changed value. This can lead to performance issues in applications with deeply nested component trees where unrelated components re-render unnecessarily.

Unstated-next addresses this by implementing a proper subscription model. When a component calls useContainer, it only re-renders when the specific values it accessed change. If a component only reads the count property from a container, updates to other properties won't trigger a re-render.

This granular reactivity becomes increasingly important as applications grow. In a Next.js application with many interactive components, preventing unnecessary re-renders can mean the difference between smooth user interactions and janky performance.

Integrating with Next.js

Next.js applications have unique state management requirements due to their hybrid server-client architecture. State that should persist across navigation needs careful handling, while server components and the App Router introduce new considerations for client-side state management.

Client-Side State Patterns

In Next.js with the App Router, components are server components by default. Any component using unstated-next must be marked with 'use client' directive since state management inherently requires client-side execution. This means strategically deciding which parts of your application need client-side state and which can remain server-only.

For page-level state that should persist across route changes, consider placing container providers at a high level in your component tree. This ensures state continuity as users navigate between pages.

Best Practices for Next.js State

  • Keep containers focused on client-side concerns
  • Fetch server-rendered data on the server and pass as props
  • Use containers only for truly client-side state like user interactions and UI state
  • Initialize containers with server data to avoid hydration mismatches

Consider the hydration strategy carefully. State that depends on server-rendered HTML should be initialized in a way that avoids hydration mismatches. The container initializer can receive initial values from props, allowing you to seed state with server data while still maintaining reactivity.

Comparison with Alternatives

Understanding how unstated-next compares to other state management options helps in making informed decisions about which tool to use.

unstated-next vs. Context API

The Context API is built into React and requires no external dependencies. It's the simplest option for sharing state across components but lacks the fine-grained reactivity that unstated-next provides. For small applications or state that changes infrequently, Context API is often sufficient. For larger applications with performance requirements, unstated-next's subscription model offers meaningful advantages.

unstated-next vs. Redux

Redux provides a powerful ecosystem with dev tools, middleware, and patterns for complex state management. However, this power comes with complexity: action types, reducers, selectors, and middleware create a significant learning curve. Unstated-next offers a much simpler alternative for applications that don't need Redux's advanced features, trading ecosystem depth for simplicity and bundle size.

unstated-next vs. Zustand

Zustand occupies a middle ground between unstated-next's minimalism and Redux's feature set. It provides a simple API while offering more features than unstated-next, including dev tools integration. Zustand's API is slightly more complex than unstated-next but remains approachable. The choice between them often comes down to specific feature needs and API preferences.

State Management Library Comparison
Featureunstated-nextContext APIReduxZustand
Bundle Size~200 bytes0 bytes (built-in)~40KB~1KB
Learning CurveLowLowHighLow
Fine-grained ReactivityYesNoYes (with selectors)Yes
Dev ToolsNoNoYesYes
MiddlewareNoNoYesLimited
TypeScript SupportYesYesYesYes

Best Practices for Scalable State Architecture

Building maintainable state management requires thoughtful organization that scales with your application.

Directory Structure

Organize containers in a dedicated directory, typically alongside the components that use them or in a centralized state directory for shared containers. Each container should have a clear responsibility and be named descriptively.

src/
 components/
 counter/
 Counter.tsx
 useCounter.ts
 state/
 user/
 UserContainer.ts
 useUser.ts
 cart/
 CartContainer.ts
 useCart.ts

This organization makes it easy to find container definitions and understand which state is available where.

TypeScript Integration

Unstated-next works well with TypeScript, and type safety becomes increasingly valuable as applications grow. Define types for your container state and return values to enable autocomplete and compile-time error checking.

interface CounterState {
 count: number;
 increment: () => void;
 decrement: () => void;
}

function useCounter(initialValue = 0): CounterState {
 const [count, setCount] = useState(initialValue);
 return {
 count,
 increment: () => setCount(c => c + 1),
 decrement: () => setCount(c => c - 1),
 };
}

const CounterContainer = createContainer(useCounter);

Testing Container Logic

Because containers are built from React hooks, testing them follows familiar patterns. You can test container logic by calling the hook function directly or by testing components that use the container. This approach keeps testing simple and focused on the container's behavior rather than implementation details.

Testing Container Logic
1import { renderHook, act } from '@testing-library/react';2import { useCounter } from './useCounter';3 4test('increments counter', () => {5 const { result } = renderHook(() => useCounter(0));6 7 act(() => {8 result.current.increment();9 });10 11 expect(result.current.count).toBe(1);12});13 14test('decrements counter', () => {15 const { result } = renderHook(() => useCounter(10));16 17 act(() => {18 result.current.decrement();19 });20 21 expect(result.current.count).toBe(9);22});

Frequently Asked Questions

Conclusion

Unstated-next represents a thoughtful approach to state management that prioritizes simplicity, performance, and alignment with React's core abstractions. For modern web development with Next.js, it offers a compelling alternative to more complex solutions when your state management needs don't require the full Redux ecosystem.

The library's tiny footprint, minimal API, and fine-grained reactivity make it well-suited for applications that value performance and simplicity. By embracing React's hooks paradigm rather than replacing it, unstated-next keeps your codebase close to React fundamentals while providing just enough structure to organize shared state effectively.

As with any technical choice, the right tool depends on your specific requirements. Unstated-next excels when you need lightweight state management without sacrificing performance, when you want to minimize bundle size, or when you prefer solutions that stay close to React's core. For complex React applications requiring sophisticated patterns, other solutions may be more appropriate. The key is matching your choice to your needs rather than defaulting to the most popular option.

If you're building a new React application and want to keep your bundle size minimal while maintaining clean, testable state management, unstated-next deserves serious consideration. Ready to build high-performance React applications with the right architecture for your needs? Our team specializes in modern React development and can help you choose and implement the optimal solutions for your project.

Ready to Build High-Performance React Applications?

Our team specializes in modern React development using Next.js and the right tools for your project's needs.

Sources

  1. GitHub - jamiebuilds/unstated-next - Official repository documentation
  2. LogRocket - Understanding state management in Next.js - State management patterns in modern React