Field note / react

publishedupdated 2026-05-04#react#context#zustand#redux-toolkit#state-management

Context vs Zustand vs Redux Toolkit

How I choose between Context, Zustand, and Redux Toolkit in React apps with different coordination, performance, and tooling needs.

TL;DR

  • Context is a delivery mechanism, not a universal state store.
  • Zustand is a great fit when I need lightweight shared client state with selectors and low ceremony.
  • Redux Toolkit is strongest when the team needs explicit event history, centralized workflows, middleware, and durable tooling.
  • The best choice depends more on coordination complexity and team needs than on benchmark arguments.

Context

Most “what state library should we use?” debates are asked too early.

The better questions are:

  • how widely shared is the state?
  • how frequently does it change?
  • does the team need event-level traceability?
  • do we need middleware or async orchestration?
  • how expensive is a wrong state transition?

Once I answer those, the library choice usually becomes obvious.

The Approach

1. Start with Context when the state is broad but low-frequency

Good Context use cases:

  • theme
  • authenticated user summary
  • tenant selection
  • feature flags
  • top-level configuration
const TenantContext = createContext<{ tenantId: string; setTenantId: (tenantId: string) => void } | null>(null)

function TenantProvider({ children }: { children: React.ReactNode }) {
  const [tenantId, setTenantId] = useState('acme')

  const value = useMemo(() => ({ tenantId, setTenantId }), [tenantId])

  return <TenantContext.Provider value={value}>{children}</TenantContext.Provider>
}

Context starts hurting when teams put high-churn state into a giant provider and rerender half the tree.

2. Reach for Zustand when shared client state should stay simple

I like Zustand when I want:

  • tiny setup
  • direct selectors
  • no provider ceremony
  • moderate shared client-state complexity
import { create } from 'zustand'

type FilterState = {
  search: string
  status: 'all' | 'open' | 'closed'
  setSearch: (search: string) => void
  setStatus: (status: FilterState['status']) => void
}

export const useFilterStore = create<FilterState>((set) => ({
  search: '',
  status: 'all',
  setSearch: (search) => set({ search }),
  setStatus: (status) => set({ status }),
}))
function SearchBox() {
  const search = useFilterStore((state) => state.search)
  const setSearch = useFilterStore((state) => state.setSearch)

  return <input value={search} onChange={(event) => setSearch(event.target.value)} />
}

This works well for dashboard filters, cross-pane selection, lightweight workspace state, and micro-frontend shells that need a small client-owned coordination layer.

3. Reach for Redux Toolkit when explicit events and tooling matter

Redux Toolkit earns its keep when the app has:

  • lots of state transitions
  • multi-step workflows
  • middleware needs
  • audit/debug expectations
  • multiple engineers touching the same coordination layer
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

type CheckoutState = {
  step: 'cart' | 'shipping' | 'payment' | 'review'
  submitting: boolean
  error: string | null
}

const initialState: CheckoutState = {
  step: 'cart',
  submitting: false,
  error: null,
}

const checkoutSlice = createSlice({
  name: 'checkout',
  initialState,
  reducers: {
    movedToStep(state, action: PayloadAction<CheckoutState['step']>) {
      state.step = action.payload
    },
    submitStarted(state) {
      state.submitting = true
      state.error = null
    },
    submitFailed(state, action: PayloadAction<string>) {
      state.submitting = false
      state.error = action.payload
    },
  },
})

For complex workflows, the event log and middleware hooks matter more than minimal code size.

4. Use a decision table instead of vibes

NeedContextZustandRedux Toolkit
Low-frequency global stateStrong fitGood fitUsually overkill
High-churn shared stateWeak fitStrong fitStrong fit
Minimal ceremonyMediumStrongWeak
Devtools/event traceabilityWeakMediumStrong
Middleware/ecosystemWeakMediumStrong
Large team coordinationWeak to mediumMediumStrong

5. Do not use any of them for server state caching by default

This is the trap I keep calling out.

If the data came from the backend and needs:

  • cache freshness
  • retries
  • invalidation
  • background refetch

I prefer a query/data layer instead of treating it like app-owned client state.

Trade-offs

  • Context is built in and simple, but it performs poorly as a general-purpose high-frequency state bus.
  • Zustand keeps shared state lightweight and ergonomic, but teams still need discipline around boundaries and derived state.
  • Redux Toolkit gives the strongest structure and tooling, but it adds ceremony that small teams often do not need.
  • Over-standardizing on one option across every surface usually hurts more than it helps.

References