On this page
Choosing a state library is easier after deciding which state should exist at all. A search query may belong in the URL, a draft belongs near its form, and product records may belong in a query cache. Putting everything in one store creates synchronization work no library can remove.
Inventory state before comparing libraries
List each state value, its owner, its readers, and how long it should survive. Distinguish server state, which has an external authority, from client state such as an open inspector or a local selection. A cached invoice and an unsaved invoice draft may look similar but have different invalidation rules.
Use the JSON Formatter to inspect a sanitized snapshot of your proposed store. Deeply nested records, duplicated totals, and copies of the same user in several branches are warning signs. Formatting makes duplication visible; it does not normalize the model automatically.
Start with local useState or useReducer when one subtree owns the behavior. Lift state only as far as coordination requires. State locality limits the number of components that can accidentally depend on an implementation detail. The right global store is often smaller than the first architecture diagram suggests.
Understand what Context actually provides
Context distributes a value through a component subtree. It is not inherently a reducer, cache, or event log. When a provider receives a different value, consumers reading that context can rerender. Wrapping a consumer in memo does not prevent an update caused by the context it reads.
That makes Context a good fit for relatively stable dependencies: theme selection, locale, or an application service. It can also support reducer-based workflows. Separate contexts for state and dispatch when components that only send actions should not subscribe to the changing state object.
import { createContext, useContext, useReducer } from 'react';
import type { Dispatch, ReactNode } from 'react';
type Action = { type: 'increment' } | { type: 'reset' };
const CountContext = createContext<number | undefined>(undefined);
const DispatchContext = createContext<Dispatch<Action> | undefined>(undefined);
export function CounterProvider({ children }: { children: ReactNode }) {
const [count, dispatch] = useReducer(
(state: number, action: Action) =>
action.type === 'increment' ? state + 1 : 0,
0
);
return (
<DispatchContext.Provider value={dispatch}>
<CountContext.Provider value={count}>{children}</CountContext.Provider>
</DispatchContext.Provider>
);
}
export function useCount() {
const count = useContext(CountContext);
if (count === undefined) throw new Error('CounterProvider is required');
return count;
}This example illustrates subscription boundaries, not a requirement to wrap every scalar in two contexts. Follow the React Context reference and keep provider placement explicit. Memoizing a provider value can prevent accidental identity changes, but it cannot make actual updates invisible to consumers that need them.
Choose Zustand for selective shared subscriptions
Zustand offers an external store with hook-based subscriptions. A component can select one slice rather than consume an entire context value. That suits editor panels, cross-route selections, or client-side tools with frequent independent updates.
The central question is not whether Zustand is lighter. Ask whether selector granularity maps to the interface. A layer list may need selectedId while a properties panel needs the selected layer record. Updating pointer coordinates should not require every panel to observe a new application object.
Use stable selectors and understand the equality behavior of the installed version. Returning a fresh object or array on every selection may cause unnecessary updates or require the documented shallow comparison helper. Do not assume examples written for an older release describe current selector requirements.
The official Zustand documentation covers selectors, middleware, and server-rendering considerations. Keep actions near the state they change. Avoid a generic setAnything escape hatch that lets every component rewrite unrelated branches and bypass domain rules.
Choose Redux Toolkit for explicit workflows
Redux Toolkit is useful when a team benefits from named actions, centralized update rules, middleware, and inspectable transitions. Complex collaborative workflows or business processes can justify that structure even if the interface has relatively few screens.
Traceable transitions help answer why state changed, not just what its current value is. A checkout may move through shipping selection, payment authorization, and completion. Domain actions communicate those events better than a collection of unrelated setters.
Use Redux Toolkit rather than hand-writing legacy Redux boilerplate. Its reducer helpers support convenient update syntax through Immer; that does not mean arbitrary mutation outside reducers is safe. Prefer serializable state when you depend on persistence, replay, or developer tooling.
Redux's official guidance explains the recommended approach. RTK Query can manage server data if Redux is already part of the architecture. Do not maintain the same remote records in RTK Query and a second hand-managed entity store without a clear reason and invalidation contract.
Test the boundary before migrating everything
Build a small vertical slice: a searchable list, a detail panel, and an editable draft. Count actual subscriptions with React DevTools, test reset behavior, and verify what happens when the route changes. A synthetic counter benchmark says little about your application's ownership problems.
For server-rendered applications, avoid leaking one user's store into another request through a module-level singleton. Follow the framework and library's per-request initialization guidance. Request isolation is a correctness requirement, not an optional optimization. Client hydration also needs an initial snapshot consistent with server output.
Treat persistence as a separate feature. Decide which fields survive reloads, how schema versions migrate, and whether stored data should be cleared after sign-out. Never persist credentials or sensitive records simply because a middleware makes it convenient. A selected tab and an authorization token have very different risk profiles.
During migration, assign one source of truth to each field. Adapters can expose old APIs temporarily, but continuous bidirectional synchronization between stores is a trap. Add tests around domain transitions before moving components. The React collection and development guides offer adjacent implementation patterns without requiring a particular state library.
Prefer the simplest approach that meets observed coordination needs. Migration cost includes teaching selectors, reviewing persistence rules, and maintaining debugging conventions, not only replacing imports. Revisit the decision when those needs change rather than predicting every future feature.
Work through a route change
Consider an editor with a document list, selected document, and unsaved title. Keep the document identifier in the URL when users must bookmark or share the selection. Read the saved document from the server cache, and initialize a separate draft when editing begins. A toolbar can subscribe to draft status without owning another copy of the document.
Now navigate to a different document with unsaved changes. Decide whether to prompt, retain a draft by identifier, or discard it explicitly. Switching from Context to Zustand will not choose that behavior for you. Write an interaction test that edits document A, opens B, and returns to A; assert the intended title and dirty status at each step.
Finally, sign out and sign in as another account. Clear account-scoped selections and drafts before showing the new workspace. This scenario tests ownership, lifetime, and reset semantics together, making it a more useful acceptance test than checking whether a store setter updates one component.
Key takeaways
- Classify local, URL, server, and shared client state before choosing a library.
- Use Context for subtree distribution, Zustand for selective subscriptions, and Redux Toolkit for explicit workflows.
- Test request isolation, persistence, reset behavior, and ownership boundaries before a broad migration.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor