Store vs. Atomic State Libraries

When built-in React patterns stop scaling, you reach for a library. Redux Toolkit and Zustand give you centralized stores. Jotai and Recoil give you atoms. Here is how to think about which model fits which problem.

June 7, 20266 min read1 / 2

I spent longer than I should have trying to make React's built-in tools handle everything.

useReducer plus createContext gets you far. But there is a specific point where the codebase starts to show the seams -- and that point is consistent enough that you can watch for it.

The Three Signs

Prop drilling at scale. Not the occasional two-level pass, but props threading through five or six components because none of them own the data but all of them need it. You notice this when you start adding parameters to components not because they need the data, but because a child somewhere needs it passed through.

Context re-rendering everything. Context was designed for state that does not change often -- themes, locales, the logged-in user. When fast-changing state (search results, a counter, a live order status) goes into context, every component that reads the context re-renders. For a notification dashboard with 30 items, a single badge count update re-renders the entire tree.

State logic spread across files. There is no single file where you can look and understand what state a feature has, what events can change it, and how each event transitions between states. The answer lives across a dozen hooks and handlers, and adding a new state transition means hunting down every related piece.

When all three appear together, it is a signal that React's built-in tools have hit their ceiling for this feature.

Why Libraries Exist

Frameworks like Svelte and Solid have different answers to state than React does. React's model is intentionally minimal -- it gives you primitives and expects you to compose them. That leaves room for accidental complexity to accumulate.

Third-party state management libraries exist to fill the gap. This is not a failure of your code. It is an acknowledgment that React's built-ins are a starting point, not an endpoint.

The key principle they enforce is indirect state management: state does not change directly. It changes via named actions or events that pass through a controlled transition function. This is the same principle as events as the source of truth -- just enforced at the library level rather than by convention.

Store-Based Libraries

A store is a container that holds state and exposes a way to transition it. The state can only change through declared transitions. Arbitrary updates from anywhere are not possible.

TypeScript
// Zustand example const useCartStore = create<CartState>((set) => ({ items: [], status: 'idle', addItem: (item) => set(s => ({ items: [...s.items, item] })), removeItem: (itemId) => set(s => ({ items: s.items.filter(i => i.id !== itemId) })), clearCart: () => set({ items: [], status: 'idle' }), }));

Components subscribe to the slice of state they need:

TSX
function CartBadge() { // Only re-renders when items.length changes const count = useCartStore(s => s.items.length); return <span>{count}</span>; } function CheckoutButton() { // Only re-renders when status changes const { status, clearCart } = useCartStore(s => ({ status: s.status, clearCart: s.clearCart })); return <button disabled={status === 'submitting'} onClick={clearCart}>Checkout</button>; }

CartBadge and CheckoutButton share the same store but subscribe to different slices. An update to status does not re-render CartBadge. An update to items does not re-render CheckoutButton.

This is the fundamental advantage of stores over context: selective subscriptions.

Stores are the right tool when:

  • State transitions have business rules (a status can only move idle → loading → success, never backwards)
  • Invalid combinations must be impossible (item count cannot be negative, a booking cannot exist without a date)
  • The logic needs to be tested independently of React (pure transition functions are trivially testable)

Libraries in this category: Redux Toolkit, Zustand, XState Store, MobX.

Atomic Libraries

Atoms are standalone, reactive pieces of state. There is no central store. Each atom holds one value, and any component can read or write it.

TypeScript
// Jotai example const cartItemsAtom = atom<CartItem[]>([]); const flashSaleAtom = atom<boolean>(false); // Derived atom: automatically recomputes when dependencies change const totalPriceAtom = atom((get) => { const items = get(cartItemsAtom); const onSale = get(flashSaleAtom); const subtotal = items.reduce((sum, i) => sum + i.price, 0); return onSale ? subtotal * 0.5 : subtotal; });

Components subscribe to individual atoms:

TSX
function TotalPrice() { const total = useAtomValue(totalPriceAtom); return <p>Total: ${total.toFixed(2)}</p>; } function FlashSaleBanner() { const [onSale, setOnSale] = useAtom(flashSaleAtom); return onSale ? <banner>Flash sale -- 50% off!</banner> : null; }

TotalPrice re-renders only when totalPriceAtom recomputes. That happens automatically when either cartItemsAtom or flashSaleAtom changes. No selector function needed. The subscriptions are declared in the atom derivation.

Atoms are the right tool when:

  • State can change from external sources (WebSocket, timers, server-sent events)
  • You need reactive derived values (total price from items + discount + tax)
  • The state is genuinely independent and does not need guarded transitions

Libraries in this category: Jotai, Recoil, Valtio, XState Store (atoms).

Using Both Together

Stores and atoms are not competing choices. They handle different categories of state.

A real e-commerce app might use:

  • A store for the cart: it has business rules (cannot add more than stock), transitions (adding → reviewing → checkout → confirmed), and needs to prevent impossible states
  • An atom for the flash sale flag: it comes from a WebSocket, lives independently, has no business rules, and just needs to be readable anywhere

Two-column diagram: left shows store-based approach with central reducer container, controlled transitions, and selective subscriptions from multiple components; right shows atomic approach with independent reactive atoms that compose and combine automatically into derived values ExpandTwo-column diagram: left shows store-based approach with central reducer container, controlled transitions, and selective subscriptions from multiple components; right shows atomic approach with independent reactive atoms that compose and combine automatically into derived values

The decision is about what the state is, not which library to pick. If the state has rules and transitions, use a store. If it is an independent reactive piece, use an atom. Many libraries -- including XState Store -- support both models in the same package.

The next post shows what this looks like in code with XState Store.

The Essentials

  1. Three signs React's built-ins have hit their limit: prop drilling through 5+ levels, context causing full-tree re-renders on fast-changing state, and state logic scattered across files with no single source of truth.
  2. Store-based libraries enforce indirect state management. State can only change via named transitions. Components subscribe to exactly the slice they need. Invalid combinations are ruled out by the transition function.
  3. Atomic libraries are for reactive, independent pieces of state. Atoms compose: a derived atom subscribes to its dependencies automatically. Use them for external data sources, derived totals, or any state that does not need guarded transitions.

Further Reading and Watching