React State: Redux vs Zustand vs Jotai (2026)

Compare Redux Toolkit, Zustand, and Jotai for React state management. Learn which library fits your project size, performance needs, and team skill level.

Inzimam Ul Haq
Inzimam Ul Haq Design Engineer
· 8 min
React logo representing modern frontend state management
Photo by Lautaro Andreani on Unsplash

TL;DR

  • Use Zustand for most React apps. Simple API, 1.16KB, no Provider needed.
  • Use Redux Toolkit for large enterprise teams (10+ devs) needing strict patterns and time-travel debugging.
  • Use Jotai when you need fine-grained reactivity with lots of derived/computed state.
  • Use TanStack Query for server state (API data). Don’t mix it with client state.
  • Skip Context API for frequently updating state. It causes unnecessary re-renders.

For most React apps in 2026, Zustand offers the best balance of simplicity and power. Use Redux Toolkit for large teams needing strict patterns; use Jotai for atomic, bottom-up state.

After migrating three enterprise applications from Redux to Zustand and building two greenfield projects with Jotai, I’ve developed a clear framework for choosing between these libraries. The “best” choice depends on your project’s complexity, team size, and what kind of state you’re managing. If you’re still deciding between frameworks entirely, check out my Angular vs React comparison for a broader perspective.

Quick Comparison

Before diving deep, here’s the data you need to make a decision:

Bundle Size and Adoption (January 2026)

LibraryMinifiedGzippedWeekly npm DownloadsGitHub Stars
Redux Toolkit43.2KB13.8KB4.2M10.5K
Zustand3.8KB1.16KB5.1M48K
Jotai5.2KB2.1KB1.8M19K

Feature Comparison

FeatureRedux ToolkitZustandJotai
Provider RequiredYesNoOptional
BoilerplateMediumMinimalMinimal
Learning CurveModerateEasyEasy
DevToolsExcellentGoodGood
TypeScriptExcellentExcellentExcellent
Derived StateManual selectorsManual selectorsBuilt-in atoms
Async SupportThunks/RTK QueryMiddlewareNative Suspense
SSR/Next.jsSupportedSupportedSupported
React 19 CompatibleYesYesYes
Best ForEnterprise appsMost appsFine-grained reactivity

Decision Flowchart

React State Management Decision Flowchart - A decision tree showing when to use TanStack Query for server state, Redux Toolkit for large teams, Jotai for fine-grained derived state, or Zustand for most applications

What is Zustand?

Zustand is a lightweight React state management library that provides global state with a hook-based API. Created by the pmndrs collective (the team behind React Three Fiber and Drei), Zustand requires no Provider wrapper, uses selectors for optimized re-renders, and supports middleware for persistence and DevTools integration. If you’re new to React hooks, my guide to mastering React hooks covers the fundamentals you’ll need.

The name comes from German, meaning “state.”

Why Zustand Wins for Most Projects

Zustand’s philosophy is “just enough structure.” You get a centralized store with hooks, but without providers, reducers, or action types:

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

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

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

export const useCartStore = create<CartStore>((set, get) => ({
  items: [],
  
  addItem: (item) => set((state) => {
    const existing = state.items.find(i => i.id === item.id);
    return {
      items: existing
        ? state.items.map(i => i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i)
        : [...state.items, { ...item, quantity: 1 }],
    };
  }),
  
  removeItem: (id) => set((state) => ({
    items: state.items.filter(i => i.id !== id),
  })),
  
  total: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
}));
// Usage: No Provider needed
import { useCartStore } from '../store/useCartStore';

function Cart() {
  const { items, removeItem, total } = useCartStore();
  return (
    <div>
      {items.map(item => (
        <div key={item.id}>
          {item.name} x {item.quantity}
          <button onClick={() => removeItem(item.id)}>Remove</button>
        </div>
      ))}
      <p>Total: ${total().toFixed(2)}</p>
    </div>
  );
}

That’s it. No Provider wrapper, no configureStore, no useDispatch.

Zustand Selectors Prevent Re-renders

Zustand re-renders components only when selected state changes. This selector pattern is key to React performance optimization:

// Only re-renders when items.length changes
const itemCount = useCartStore((state) => state.items.length);

// Only re-renders when specific item changes
const firstItem = useCartStore((state) => state.items[0]);

// Re-renders on any store change (avoid this)
const store = useCartStore();

Zustand Middleware

Add persistence, DevTools, and more with middleware:

import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

export const useCartStore = create<CartStore>()(
  devtools(
    persist(
      (set, get) => ({
        // ... store implementation
      }),
      { name: 'cart-storage' }
    ),
    { name: 'CartStore' }
  )
);

Zustand Trade-offs

ProsCons
90% less code than classic ReduxLess structure can lead to inconsistent patterns
1.16KB gzippedNo built-in data fetching
No Provider requiredFewer guardrails for large codebases
Built-in selector optimization
Works outside React (vanilla JS)

What is Redux Toolkit?

Redux Toolkit (RTK) is the official, modern way to write Redux. It eliminates boilerplate with createSlice and configureStore, includes RTK Query for data fetching, and provides time-travel debugging via Redux DevTools. Redux follows the Flux architecture pattern with unidirectional data flow and immutable state updates.

When Redux Toolkit Shines

RTK is the right choice when you need:

  • Strict architectural patterns that scale across 10+ developers
  • Time-travel debugging for complex state interactions
  • Extensive middleware ecosystem for logging, persistence, and side effects
  • RTK Query for integrated data fetching with caching
// store/slices/cartSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CartState {
  items: CartItem[];
}

const cartSlice = createSlice({
  name: 'cart',
  initialState: { items: [] } as CartState,
  reducers: {
    addItem: (state, action: PayloadAction<Omit<CartItem, 'quantity'>>) => {
      const existing = state.items.find(item => item.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(item => item.id !== action.payload);
    },
  },
});

export const { addItem, removeItem } = cartSlice.actions;
export default cartSlice.reducer;
// Usage: Requires Provider and hooks
import { useSelector, useDispatch } from 'react-redux';
import { removeItem } from '../store/slices/cartSlice';

function Cart() {
  const items = useSelector((state: RootState) => state.cart.items);
  const dispatch = useDispatch();
  
  return (
    <div>
      {items.map(item => (
        <button key={item.id} onClick={() => dispatch(removeItem(item.id))}>
          Remove {item.name}
        </button>
      ))}
    </div>
  );
}

RTK Query for Data Fetching

If you’re using Redux, RTK Query provides TanStack Query-like features:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const productsApi = createApi({
  reducerPath: 'productsApi',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Products'],
  endpoints: (builder) => ({
    getProducts: builder.query<Product[], void>({
      query: () => 'products',
      providesTags: ['Products'],
    }),
  }),
});

export const { useGetProductsQuery } = productsApi;

Redux Toolkit Trade-offs

ProsCons
Battle-tested at massive scaleMore boilerplate than alternatives
Excellent time-travel debugging13.8KB gzipped
Strong TypeScript supportLearning curve for new developers
RTK Query for data fetchingOverkill for simple applications
Enforced patterns prevent driftRequires Provider wrapper

What is Jotai?

Jotai is an atomic React state management library where state is broken into independent “atoms.” Components subscribe to specific atoms and only re-render when those atoms change. Created by the same team behind Zustand (pmndrs), Jotai excels at derived state and integrates natively with React Suspense for async operations.

The name comes from Japanese (状態), meaning “state.”

The Atomic Mental Model

Think of atoms as the smallest units of state. Each atom is independent, and derived atoms automatically track dependencies:

// atoms/cartAtoms.ts
import { atom } from 'jotai';

// Primitive atom
export const cartItemsAtom = atom<CartItem[]>([]);

// Derived atom (read-only, auto-updates)
export const cartTotalAtom = atom((get) => {
  const items = get(cartItemsAtom);
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
});

// Derived atom (computed count)
export const cartCountAtom = atom((get) => {
  return get(cartItemsAtom).reduce((sum, item) => sum + item.quantity, 0);
});

// Write-only atom (action)
export const addItemAtom = atom(null, (get, set, item: Omit<CartItem, 'quantity'>) => {
  const items = get(cartItemsAtom);
  const existing = items.find(i => i.id === item.id);
  
  set(cartItemsAtom, existing
    ? items.map(i => i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i)
    : [...items, { ...item, quantity: 1 }]
  );
});
// Usage: Granular subscriptions
import { useAtomValue, useSetAtom } from 'jotai';
import { cartItemsAtom, cartTotalAtom, addItemAtom } from '../atoms/cartAtoms';

function CartTotal() {
  // Only re-renders when total changes
  const total = useAtomValue(cartTotalAtom);
  return <p>Total: ${total.toFixed(2)}</p>;
}

function AddToCart({ product }) {
  // Never re-renders from cart changes
  const addItem = useSetAtom(addItemAtom);
  return <button onClick={() => addItem(product)}>Add</button>;
}

Jotai Async Atoms with Suspense

Jotai handles async state elegantly with React Suspense. This pattern works seamlessly with React Server Components:

const userIdAtom = atom(1);

// Async derived atom
const userDataAtom = atom(async (get) => {
  const id = get(userIdAtom);
  const response = await fetch(`/api/users/${id}`);
  return response.json();
});

// Usage with Suspense
<Suspense fallback={<Loading />}>
  <UserProfile /> {/* useAtomValue(userDataAtom) inside */}
</Suspense>

Jotai Trade-offs

ProsCons
Minimal re-renders (atomic subscriptions)Requires different mental model
Built-in derived stateAtoms can scatter across files
2.1KB gzippedLess obvious data flow
Native Suspense integrationDevTools less mature than Redux
No Provider required

React Context API: When It’s Enough

When should you use Context API instead of a state library? Context API works for state that changes infrequently and doesn’t require optimized re-renders.

Context API vs State Libraries

Use Context APIUse Zustand/Redux/Jotai
Theme (light/dark mode)Shopping cart state
User locale/languageForm data
Auth status (logged in/out)Real-time updates
Feature flagsFrequently changing UI state

Why Context Causes Performance Issues

Context API triggers re-renders in all consuming components when any value changes. There’s no built-in way to subscribe to just part of the context. For a deeper dive into re-render optimization, see my React performance optimization guide:

// Problem: Every consumer re-renders when ANY value changes
const AppContext = createContext({ user: null, theme: 'light', notifications: [] });

function Header() {
  const { theme } = useContext(AppContext); // Re-renders when notifications change!
  return <header className={theme}>...</header>;
}

State management libraries solve this with selectors (Zustand), atoms (Jotai), or memoized selectors (Redux). When building interactive UI components, choosing the right state approach directly impacts user experience.

Server State: Use TanStack Query

What is server state? Server state is data that originates from an external source (APIs, databases) and needs synchronization between client and server.

None of the client state libraries (Zustand, Redux, Jotai) are ideal for server state. Data from APIs needs:

  • Caching with configurable stale times
  • Background refetching
  • Optimistic updates
  • Request deduplication
  • Loading and error states

TanStack Query Example

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

function Products() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['products'],
    queryFn: () => fetch('/api/products').then(r => r.json()),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });

  if (isLoading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  
  return <ProductList products={data} />;
}

Best practice: Use TanStack Query (or RTK Query) for server state, and Zustand/Jotai for client state. They complement each other.

Migration Guide: Redux to Zustand

Many teams are migrating from Redux to Zustand. Here’s the incremental approach:

Step 1: Install Zustand Alongside Redux

npm install zustand

Step 2: Convert One Slice at a Time

// Before: Redux slice
const userSlice = createSlice({
  name: 'user',
  initialState: { user: null, loading: false },
  reducers: {
    setUser: (state, action) => { state.user = action.payload },
    setLoading: (state, action) => { state.loading = action.payload },
  },
});

// After: Zustand store
const useUserStore = create((set) => ({
  user: null,
  loading: false,
  setUser: (user) => set({ user }),
  setLoading: (loading) => set({ loading }),
}));

Step 3: Update Components

// Before
const user = useSelector((state) => state.user.user);
const dispatch = useDispatch();
dispatch(setUser(newUser));

// After
const user = useUserStore((state) => state.user);
const setUser = useUserStore((state) => state.setUser);
setUser(newUser);

Step 4: Remove Redux Slice

Once all components are migrated, remove the Redux slice and its references.

Typical results: 70-90% code reduction, simpler mental model, faster onboarding for new developers.

SSR and Next.js Compatibility

All three libraries work with Next.js App Router and React Server Components, but require different patterns for SSR hydration.

Zustand with Next.js

// For SSR, use createStore + context pattern
import { createStore } from 'zustand';
import { createContext, useContext, useRef } from 'react';

const StoreContext = createContext(null);

export function StoreProvider({ children, initialState }) {
  const storeRef = useRef();
  if (!storeRef.current) {
    storeRef.current = createStore((set) => ({
      ...initialState,
      // ... actions
    }));
  }
  return (
    <StoreContext.Provider value={storeRef.current}>
      {children}
    </StoreContext.Provider>
  );
}

Jotai with Next.js

// Jotai has built-in SSR support
import { Provider } from 'jotai';
import { useHydrateAtoms } from 'jotai/utils';

function HydrateAtoms({ initialValues, children }) {
  useHydrateAtoms(initialValues);
  return children;
}

export function Providers({ children, initialState }) {
  return (
    <Provider>
      <HydrateAtoms initialValues={[[cartAtom, initialState.cart]]}>
        {children}
      </HydrateAtoms>
    </Provider>
  );
}

React 19 and React Compiler Compatibility

All three libraries are compatible with React 19 and the React Compiler (formerly React Forget).

React Compiler impact:

  • Zustand: Selectors continue to work; compiler may optimize component re-renders further
  • Redux: Selectors and memoization patterns remain valid
  • Jotai: Atomic subscriptions already minimize re-renders; compiler provides additional optimization

The use() hook: React 19’s use() hook for promises works alongside these libraries but doesn’t replace them for client state management.

Next Steps

  1. Starting a new project? Begin with Zustand. It covers 90% of use cases with minimal complexity.

  2. On a large enterprise team? Evaluate Redux Toolkit for its enforced patterns and time-travel debugging.

  3. Need fine-grained reactivity? Try Jotai for components with complex derived state.

  4. Managing API data? Add TanStack Query regardless of which client state library you choose.

  5. Migrating from Redux? Follow the incremental migration guide above. Start with one slice.

  6. Building interactive components? Pair your state management with Framer Motion for smooth animations.

  7. Working with designers? Learn how to bridge design and code for better component architecture.

Additional Resources

Frequently Asked Questions

Which React state management library should I use in 2026?
For most React apps, Zustand offers the best balance of simplicity and power. Use Redux Toolkit for large enterprise teams needing strict patterns and time-travel debugging. Use Jotai for atomic, bottom-up state where you need fine-grained reactivity and minimal re-renders.
Is Redux still relevant in 2026?
Yes, but its role has narrowed. Redux Toolkit remains excellent for large enterprise applications with complex state relationships, strict architectural requirements, and teams that benefit from enforced patterns. For smaller to medium projects, Zustand or Jotai are often better choices.
What's the difference between Zustand and Jotai?
Zustand uses a centralized, top-down store approach similar to Redux but with minimal boilerplate. Jotai uses an atomic, bottom-up approach where state is broken into independent atoms. Zustand is better for interconnected state; Jotai excels at fine-grained reactivity with minimal re-renders.
Should I use Context API for state management?
Context API works fine for low-frequency updates like themes or user preferences. However, it causes all consuming components to re-render on any context change, making it unsuitable for frequently updating state. Use a dedicated state library for dynamic data.
What about server state like API data?
Server state (data from APIs) should be handled separately from client state. Use TanStack Query (React Query) or RTK Query for fetching, caching, and synchronizing server data. These libraries handle loading states, caching, background refetching, and error handling automatically.
Can I use Zustand with Next.js App Router?
Yes. Zustand works with Next.js App Router and React Server Components. For SSR hydration, use Zustand's createStore with a context provider pattern to avoid shared state between requests.
How do I migrate from Redux to Zustand?
Migrate incrementally: 1) Install Zustand alongside Redux, 2) Convert one slice at a time to a Zustand store, 3) Update components to use the new store, 4) Remove the Redux slice once migrated. Most teams report 70-90% code reduction.
What is Zustand?
Zustand is a lightweight React state management library (1.16KB gzipped) that provides global state with a hook-based API. Created by the pmndrs collective, it requires no Provider wrapper, uses selectors for optimized re-renders, and supports middleware for persistence and DevTools.
What is Jotai?
Jotai is an atomic React state management library (2.1KB gzipped) where state is broken into independent atoms. Components subscribe to specific atoms and only re-render when those atoms change. Jotai excels at derived state and integrates natively with React Suspense.
What is Redux Toolkit?
Redux Toolkit (RTK) is the official, modern way to write Redux. It eliminates boilerplate with createSlice and configureStore, includes RTK Query for data fetching, and provides time-travel debugging via Redux DevTools. Bundle size is 13.8KB gzipped.

Sources & References