The State Management Landscape in 2026

React's built-in tools — useState, useReducer, and the Context API — handle local and lightweight global state well. But when multiple unrelated components need to share frequently-updating state, or when you need time-travel debugging and strict action logging, a dedicated state management library becomes the right tool. The two dominant choices are Zustand (minimal, 1 KB) and Redux Toolkit (full-featured, ~50 KB). Choosing between them is a team and complexity trade-off, not a performance one.

Zustand: Simple Global State

Zustand is a small state management library built on a subscription model. You define a store as a function that receives a set callback and returns an object with state and actions. No boilerplate, no reducers, no action creators.

Basic Zustand Store

// store/useCartStore.ts
import { create } from 'zustand'

interface CartItem {
  id: string
  name: string
  price: number
  quantity: number
}

interface CartStore {
  items: CartItem[]
  total: number
  addItem: (item: Omit<CartItem, 'quantity'>) => void
  removeItem: (id: string) => void
  updateQuantity: (id: string, quantity: number) => void
  clearCart: () => void
}

export const useCartStore = create<CartStore>()((set, get) => ({
  items: [],
  total: 0,

  addItem: (item) => set((state) => {
    const existing = state.items.find(i => i.id === item.id)
    const items = existing
      ? state.items.map(i => i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i)
      : [...state.items, { ...item, quantity: 1 }]
    return { items, total: items.reduce((sum, i) => sum + i.price * i.quantity, 0) }
  }),

  removeItem: (id) => set((state) => {
    const items = state.items.filter(i => i.id !== id)
    return { items, total: items.reduce((sum, i) => sum + i.price * i.quantity, 0) }
  }),

  updateQuantity: (id, quantity) => set((state) => {
    const items = state.items.map(i => i.id === id ? { ...i, quantity } : i)
    return { items, total: items.reduce((sum, i) => sum + i.price * i.quantity, 0) }
  }),

  clearCart: () => set({ items: [], total: 0 }),
}))

// Usage in any component — no Provider needed
function CartButton() {
  const total = useCartStore(state => state.total) // Subscribes only to total
  const addItem = useCartStore(state => state.addItem) // Stable function reference
  return <button onClick={() => addItem({ id: '1', name: 'Widget', price: 9.99 })}>
    Cart (${total.toFixed(2)})
  </button>
}

Zustand Middleware: Persist and Immer

import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'

// Persist state to localStorage
export const useSettingsStore = create(
  persist(
    immer<SettingsStore>()((set) => ({
      theme: 'dark',
      language: 'en',
      setTheme: (theme) => set((state) => { state.theme = theme }), // Immer: mutate directly
      setLanguage: (lang) => set((state) => { state.language = lang }),
    })),
    {
      name: 'app-settings',  // localStorage key
      partialize: (state) => ({ theme: state.theme, language: state.language }),
    }
  )
)

Redux Toolkit: Structured State with DevTools

Redux Toolkit (RTK) is the official, opinionated way to write Redux. It eliminates the boilerplate of classic Redux (action types, action creators, switch reducers) while keeping Redux's architecture: a single store, unidirectional data flow, and excellent DevTools for time-travel debugging.

RTK Slice

// store/cartSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'

interface CartItem { id: string; name: string; price: number; quantity: number }
interface CartState { items: CartItem[]; status: 'idle' | 'loading' | 'failed' }

// Async thunk for API calls
export const fetchSavedCart = createAsyncThunk(
  'cart/fetchSaved',
  async (userId: string) => {
    const response = await fetch(`/api/carts/${userId}`)
    return response.json() as Promise<CartItem[]>
  }
)

const cartSlice = createSlice({
  name: 'cart',
  initialState: { items: [], status: 'idle' } as CartState,
  reducers: {
    addItem(state, action: PayloadAction<Omit<CartItem, 'quantity'>>) {
      // Immer is built into RTK — mutate directly
      const existing = state.items.find(i => i.id === action.payload.id)
      if (existing) {
        existing.quantity += 1
      } else {
        state.items.push({ ...action.payload, quantity: 1 })
      }
    },
    removeItem(state, action: PayloadAction<string>) {
      state.items = state.items.filter(i => i.id !== action.payload)
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchSavedCart.pending, (state) => { state.status = 'loading' })
      .addCase(fetchSavedCart.fulfilled, (state, action) => {
        state.status = 'idle'
        state.items = action.payload
      })
      .addCase(fetchSavedCart.rejected, (state) => { state.status = 'failed' })
  },
})

export const { addItem, removeItem } = cartSlice.actions
export default cartSlice.reducer

// store/index.ts
import { configureStore } from '@reduxjs/toolkit'
import cartReducer from './cartSlice'

export const store = configureStore({
  reducer: { cart: cartReducer }
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

Using RTK in Components

// Wrap your app with Provider
// app/layout.tsx
import { Provider } from 'react-redux'
import { store } from '@/store'

export default function RootLayout({ children }) {
  return <Provider store={store}>{children}</Provider>
}

// In a component
import { useSelector, useDispatch } from 'react-redux'
import { addItem, removeItem } from '@/store/cartSlice'
import type { RootState, AppDispatch } from '@/store'

function CartComponent() {
  const items = useSelector((state: RootState) => state.cart.items)
  const dispatch = useDispatch<AppDispatch>()

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          {item.name} × {item.quantity}
          <button onClick={() => dispatch(removeItem(item.id))}>Remove</button>
        </li>
      ))}
    </ul>
  )
}

When to Use Which

CriterionZustandRedux Toolkit
Bundle size~1 KB~50 KB
BoilerplateMinimalModerate (slices)
DevToolsRedux DevTools extension (opt-in)Redux DevTools (built-in)
Team sizeSmall to mediumMedium to large
Async data fetchingManual (use fetch/SWR)RTK Query (built-in)
Time-travel debuggingVia devtools middlewareFirst-class
Learning curve30 minutes1–2 days
Best forUI state, preferences, modalsComplex business logic, large teams

Performance: Selector Optimization

Both libraries re-render a component only when the selected slice of state changes. The key is selecting the smallest possible slice:

// Zustand — select individual fields (each is its own subscription)
const itemCount = useCartStore(state => state.items.length)  // Only re-renders when count changes
const total = useCartStore(state => state.total)              // Only re-renders when total changes

// Avoid selecting the whole object — re-renders on any state change
const { items, total } = useCartStore() // 🐌 Worse: re-renders whenever anything changes

// For computed values from multiple fields, use useShallow (Zustand 4+)
import { useShallow } from 'zustand/react/shallow'
const { name, email } = useUserStore(useShallow(state => ({ name: state.name, email: state.email })))

// RTK — use createSelector for memoized derived state
import { createSelector } from '@reduxjs/toolkit'
const selectItemCount = createSelector(
  (state: RootState) => state.cart.items,
  (items) => items.reduce((sum, i) => sum + i.quantity, 0)
)
// selectItemCount only recomputes when items array reference changes