AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 08 — State Management — Context, Zustand, Jotai, and When to Reach Beyond useState

08 — State Management — Context, Zustand, Jotai, and When to Reach Beyond useState

August 13, 20267 min read
Download as Markdown

A wall of library names was how I greeted state management until I saw the ladder underneath. The model that organized it: there's a ladder, and the right rung depends on two questions — how widely the state is shared, and how often it changes. [1] useState and useReducer live on one component. Context shares values across a tree but re-renders every consumer on change. When state is both widely shared and frequently changing, a dedicated store — Zustand, Jotai, or MobX — earns its keep.

The shared-state problem

Local state with useState is fine when one component owns a value. The trouble starts the moment two siblings need the same value, or a deeply-nested component needs something from the top of the tree. The textbook answer is lift the state up to the nearest common ancestor and pass it down through props. That works for a couple of levels; past that, I'm threading props through components that don't care about the value just to deliver it where it's needed — the "prop-drilling" tax.

State management is the discipline of solving that sharing problem at scale, and the libraries on the roadmap sit at different points on the complexity curve.

Context: dependency injection, not a store

React's built-in Context is the first rung past prop-drilling [2]. A Provider at the top of the tree declares a value; any descendant can useContext to read it without the intermediate components having to know about it. The canonical cases are values that are shared widely and change rarely: theme, locale, the current authenticated user.

The critical caveat, and the one that bit me: Context is not a state management library. [2] When the Provider's value changes, every component that consumes that context re-renders — even ones reading a different field. For slowly-changing values that's fine. For a counter that ticks every second, or a list that updates on every keystroke, every consumer re-renders on every change, and on a large tree that's a performance cliff. That's the signal to move up the ladder.

Zustand · Jotai · MobX — external store shared, frequently-changing state · components subscribe to slices Context — built-in shared, rarely-changing values (theme, locale, auth) useState · useReducer — local state owned by one component shared + changes often shared + changes rarely one owner escalate when prop-drilling or Context re-renders hurt

Zustand: small, hooks-based, my default store

When state genuinely needs to leave a single component, Zustand is the store I reach for first [3]. It's deliberately tiny — a store is created with one create call, and components subscribe with a hook. Two properties earned it the default slot:

  • No boilerplate, no opinion. There's no action/reducer ceremony like Redux, no provider wrapper required. I write the store as one function and call it from anywhere.
  • Fine-grained subscriptions. A component selects just the slice it needs; Zustand only re-renders that component when that specific slice changes. That fixes the Context re-render problem by design.
const useStore = create((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}));

// a component subscribes to only count
const count = useStore((s) => s.count);

Zustand sits in the "small to medium app" sweet spot — when I want a global store without Redux's ceremony, it's the answer.

Jotai: atomic state, signals-like DX

Jotai takes an atomic approach: state is broken into atoms (small units), and components subscribe to the atoms they read [4]. Because subscriptions are per-atom, renders are optimized automatically — a component only re-renders when one of its specific atoms changes. Atoms compose, so complex state is built by combining simpler ones, and the experience is close to signals — little wires connecting one piece of data to one spot on the screen — while staying declarative.

const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2);

const [count, setCount] = useAtom(countAtom);

Jotai is the choice when state is naturally a graph of derived values, or when the memoization pain of useMemo/selector functions becomes the bottleneck. It's a touch more conceptually demanding than Zustand, so I reach for it specifically when derived-state composition matters.

MobX: observable, mutable, opinionated

MobX is the elder statesman of this group, built on observable values — values that announce when they change — and automatic dependency tracking [5]. State is mutable — I write user.name = 'Ave' directly, and MobX's reactivity system re-renders the components reading user.name automatically. The tradeoff is a heavier conceptual model (observables, actions, reactions) and more "magic" than hooks-based stores. It's powerful and ergonomic once internalized, but I treat it as a team-scale choice rather than a starter store.

Redux: not on the React roadmap, but worth naming

The roadmap's React state-management section doesn't list Redux directly (it shows up in the broader frontend roadmap). I note it only because it's the historical default: a single store, actions, and reducers, with a lot of boilerplate. Redux Toolkit cut that boilerplate, and RTK Query (covered in the data-fetching notes) extended it into server-state caching. For greenfield React today I'd start with Zustand unless the team already lives in Redux.

How I use this

My state-placement checklist, in order:

  • One component owns it? → useState / useReducer. Don't over-engineer.
  • Shared across the tree, but rarely changing (theme, locale, auth session)? → Context. It's built-in and fine here.
  • Shared across the tree AND frequently changing (cart, real-time data, form state across pages)? → a dedicated store, defaulting to Zustand for its tiny API and selector-based re-renders. Jotai if the state is naturally a graph of derived atoms.
  • Server data (API responses, mutations, caching)? → that's a different problem entirely, handled by data-fetching libraries (TanStack Query, SWR), not by a state store. Mixing server cache into a Zustand store is a common mistake; the data-fetching notes cover why.

The discipline is matching the rung to the actual sharing and change-frequency profile, not reaching for the most powerful tool by default.

References

[1] R. Wieruch, "Overview of state in React," robinwieruch.de, 2023. [Online]. Available: https://www.robinwieruch.de/react-state/

[2] React team, "Passing data deeply with context," react.dev, 2024. [Online]. Available: https://react.dev/learn/passing-data-deeply-with-context

[3] pmndrs, "Zustand — getting started," docs.pmnd.rs, 2024. [Online]. Available: https://docs.pmnd.rs/zustand/getting-started/introduction

[4] Jotai team, "Jotai — atomic state management," jotai.org, 2024. [Online]. Available: https://jotai.org/

[5] MobX team, "MobX — simple, scalable state management," mobx.js.org, 2024. [Online]. Available: https://mobx.js.org/

[6] T. Kнoup, "Working with Zustand," tkdodo.eu, 2023. [Online]. Available: https://tkdodo.eu/blog/working-with-zustand

Knowledge check · Question 1 of 5

Context's main weakness as a state-management solution is…

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!