Skip to content
matthewcm.dev

    ↑ ↓ move · ↵ open · tab scopeopen graph →

    React State Management Choices

    Keep state local and atomic; use useReducer when transitions get complex, Context for low-churn values, and a selector store like Zustand or Redux when many components read fast-changing shared state.

    Seedtended planted 2 min read

    🌱 A rough idea, planted recently. Expect rough edges — it will grow.

    On this page

    Keep state as local and as atomic as possible. Reach for useReducer when transitions get complicated, Context for values that rarely change, and a selector store (Zustand, Redux) when many components read fast-changing shared state.

    The state flow

    User action, then an event, a state update, a re-render, and the UI shows the new state. Every choice below is really about how much of the tree re-renders at the state-update step.

    Atomic state

    State that changes together should update together: one { isLoading, quoteValue } object (or a reducer) instead of two useStates set one after the other. One update, one re-render, and no impossible in-between states.

    Functional updates read the latest state and drop a dependency:

    setUser((prev) => ({ ...prev, profilePicture: newPic })); // no `user` in the deps

    useReducer

    • All the transitions live in one place (dispatch({ type: "pageChanged", page })), so components get simpler and fewer props churn.
    • It pairs with Context to avoid prop drilling, and the shape scales up to a global store; Redux is the same idea.
    • A gotcha I’ve hit building a paginated table: derived state, like the current page of rows, has to be recomputed from reducer state. If the reducer mutates and returns the same object, or the derived value isn’t recalculated, nothing re-renders. Always return new objects.

    Context vs Redux vs Zustand

    Use forWatch out
    ContextLow-churn values: theme, the signed-in user, dependency injection; small subtreesEvery consumer re-renders on any change
    Redux ToolkitLarge, complex app state with strict patterns and great toolingBoilerplate; selectors keep it fast in very dynamic apps
    ZustandMy default for shared client state: small API, selector subscriptions, usable outside ReactNeeds discipline about store boundaries
    TanStack QueryServer state (cache, refetch), which isn’t client state at allDon’t copy server data into a store

    The cost of centralised state: one big store or context means unrelated changes can re-render parts of the UI that don’t care. Narrow selectors and splitting stores fix it. For something like a live price blotter, use a selector store with transient updates for the hottest values (Zustand Transient Updates).

    • #react
    • #state-management
    • #zustand
    • #redux
    • #context

    ↪ Links to (4)