Skip to main content

React Context vs Zustand vs Jotai

React Context is built into React; Zustand and Jotai are external libraries. Many teams start with Context and struggle when re-renders become a bottleneck. This article lays out the trade-offs so you can make an informed decision before you refactor.

I have debugged production performance issues in three React apps caused by overfitting to Context. In each case, moving just the frequently-changing state to Zustand eliminated unnecessary re-renders while keeping simple, rarely-changed state (like locale or user theme) in Context.

React Context: Built-In but Costly

React Context is designed for passing props down the tree without threading them through every intermediate component. It is not designed for frequently-changing state. When a Context provider updates, all children that use that Context re-render — there is no selector optimization.

const ThemeContext = createContext();

export function App() {
const [theme, setTheme] = useState('light');

return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Dashboard />
</ThemeContext.Provider>
);
}

export function Header() {
const { theme } = useContext(ThemeContext); // Re-renders on EVERY theme change
return <div className={theme}>...</div>;
}

If setTheme() is called 60 times per second (say, during a drag gesture), every component using ThemeContext re-renders 60 times too. This is fine for theme or locale (static-ish), but catastrophic for drag state, form inputs, or real-time data.

Zustand: Zero Re-Render Overhead

Zustand's selector pattern ensures only affected components re-render:

const useThemeStore = create((set) => ({
theme: 'light',
setTheme: (newTheme) => set({ theme: newTheme }),
}));

export function Header() {
const theme = useThemeStore((state) => state.theme); // Re-renders ONLY if theme changes
return <div className={theme}>...</div>;
}

export function Sidebar() {
const setTheme = useThemeStore((state) => state.setTheme); // Re-renders ONLY if setTheme reference changes (never, for bound methods)
return <button onClick={() => setTheme('dark')}>Toggle</button>;
}

Even if another part of the store updates (a counter, user data, etc.), Header does not re-render because it only selected theme.

Jotai: Atomic Re-Render Boundaries

Jotai's atoms act as re-render boundaries. Components re-render only if their atoms change:

const themeAtom = atom('light');

export function Header() {
const [theme] = useAtom(themeAtom); // Re-renders ONLY if themeAtom changes
return <div className={theme}>...</div>;
}

export function Sidebar() {
const [, setTheme] = useAtom(themeAtom); // Re-renders ONLY if themeAtom changes
return <button onClick={() => setTheme('dark')}>Toggle</button>;
}

Re-render efficiency is comparable to Zustand. The difference is in code organization: Jotai atoms are explicit values you pass around, while Zustand stores are global singletons.

Bundle Size Comparison

LibraryMinifiedGzipped
React Context0 KB (built-in)0 KB
Zustand3.2 KB2.9 KB
Jotai4.1 KB3.7 KB

For most apps, the difference is negligible. Context trades runtime performance for zero bundle cost; Zustand and Jotai trade a few KB for a much better developer experience.

Decision Matrix

ScenarioUse ContextUse ZustandUse Jotai
Theme, locale, user authYesOptionalOptional
Form state, drag, real-timeNoYesYes
Micro-frontends, code-splitNoYesYes
Complex derived stateNoOptionalYes
Suspense and Server ComponentsNoNoYes
Team knows ReduxMaybeYesNo

Key Takeaways

  • Context is ideal for static or slowly-changing state (theme, locale, user info). It has zero bundle cost but poor re-render performance if state changes frequently.
  • Zustand is best for global feature state (modals, filters, preferences). It offers fine-grained re-renders and minimal boilerplate.
  • Jotai excels when state is granular and composed (filters, metrics, derived values). It integrates well with Suspense and code-splitting.
  • Most apps use a hybrid: Context for auth + theme, Zustand or Jotai for everything else.

Frequently Asked Questions

Can I use Context with useMemo to avoid re-renders?

You can memoize the Context value to avoid recreating the object, but the Context provider itself still broadcasts updates to all consumers. useMemo only helps if the value object is recreated unnecessarily; it does not prevent Context-driven re-renders.

Should I migrate all my Context to Zustand?

No. Keep simple, stable state (user ID, theme) in Context. Migrate only frequently-changing state (forms, real-time data, drag state) to Zustand or Jotai. This hybrid approach is standard in production React apps.

Is Context slower than Zustand by a measurable amount?

For static state, Context is actually faster (zero subscription overhead). For frequently-changing state, Zustand is 5–20x faster because fewer components re-render. Profile your app to decide; do not assume Context is always slow.

Can I use Zustand and Context together?

Yes. Many teams use Context for auth and theme, Zustand for UI state, and React Query for server state. This is a healthy, scalable architecture.

Further Reading