Zustand, Redux, and Jotai aren’t just different ways to manage state; they represent fundamentally different philosophies about how your application’s data should flow and how you interact with it.

Let’s see Zustand in action. Imagine a simple counter.

import { create } from 'zustand';

const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}));

function Counter() {
  const count = useCounterStore((state) => state.count);
  const increment = useCounterStore((state) => state.increment);
  const decrement = useCounterStore((state) => state.decrement);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

In this Zustand example, create is the core function. It takes a callback that receives a set function. This set function is how you update the state. You pass it an object representing the new state, and Zustand merges it. The useCounterStore hook is what your components use to select pieces of the state and the actions that modify it. This selector mechanism is key: components only re-render if the specific piece of state they subscribed to changes.

Now, Redux. Redux is built around a single, immutable state tree and a strict unidirectional data flow. Actions are dispatched, and reducers handle how those actions change the state.

// store.js
import { configureStore } from '@reduxjs/toolkit';

const initialState = { count: 0 };

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'counter/increment':
      return { ...state, count: state.count + 1 };
    case 'counter/decrement':
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
}

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

// App.js
import React from 'react';
import { Provider } from 'react-redux';
import { store } from './store';
import CounterComponent from './CounterComponent';

function App() {
  return (
    <Provider store={store}>
      <CounterComponent />
    </Provider>
  );
}

// CounterComponent.jsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';

function CounterComponent() {
  const count = useSelector((state) => state.counter.count);
  const dispatch = useDispatch();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch({ type: 'counter/increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'counter/decrement' })}>Decrement</button>
    </div>
  );
}

Here, configureStore from Redux Toolkit sets up the store. The counterReducer is a pure function that takes the current state and an action, and returns the new state. Components use useSelector to read data from the store and useDispatch to send actions. The Provider wraps your app to make the store available. Redux’s strength is its predictability and powerful debugging tools (like Redux DevTools), but it often involves more boilerplate.

Jotai takes a different approach, focusing on "atoms" – small, independent pieces of state. It’s inspired by Recoil but aims for a simpler API.

import { atom, useAtom } from 'jotai';

const countAtom = atom(0);
const incrementAtom = atom(
  (get) => get(countAtom),
  (get, set) => set(countAtom, get(countAtom) + 1)
);
const decrementAtom = atom(
  (get) => get(countAtom),
  (get, set) => set(countAtom, get(countAtom) - 1)
);

function CounterJotai() {
  const [count, setCount] = useAtom(countAtom);
  const [, increment] = useAtom(incrementAtom);
  const [, decrement] = useAtom(decrementAtom);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

In Jotai, atom(initialValue) creates a basic piece of state. You can also create derived atoms (like incrementAtom) that read from other atoms (get) and update them (set). The useAtom hook is the primary way components interact with atoms, providing both the current value and a function to update it. Jotai’s atomic nature allows for very granular re-renders; only components using an atom that changes will re-render.

The core problem they all solve is managing application state that needs to be shared across multiple components, avoiding "prop drilling" (passing props down through many intermediate components) and ensuring that UI updates consistently when data changes.

Zustand’s magic is its hook-based API and built-in selector optimization. When you use useCounterStore((state) => state.count), Zustand internally tracks which part of the state your component needs. If only state.count changes, only components using state.count will re-render, not components that might be using other parts of the store but haven’t had their subscribed data updated. This is achieved by creating a custom hook that subscribes to the store and uses a comparison function to determine if a re-render is necessary.

Redux, on the other hand, manages state by having a single source of truth. When an action is dispatched, the reducer creates a new state object. React-Redux then compares the new state with the old state at the subscription point. If the selected slice of state has changed, the component re-renders. This immutability is crucial for predictable state changes and enables powerful time-travel debugging.

Jotai’s reactivity is driven by its atomic model. When an atom’s value changes, Jotai knows exactly which components are subscribed to that specific atom and triggers re-renders only for them. This fine-grained control means that if you have 100 atoms and only atom #5 changes, only the components using atom #5 (or atoms derived from it) will update. This is often the most performant for complex applications with many independent pieces of state.

A common misconception is that you need a complex setup for every state management solution. For instance, with Zustand, you can define multiple independent "stores" for different concerns (e.g., useUserStore, useCartStore) without needing to combine them into a single monolithic store, which can be a significant advantage for scalability and maintainability compared to the more centralized approach of Redux.

The next step in state management often involves considering asynchronous operations, complex data fetching, and caching strategies, which libraries like React Query or RTK Query can integrate with these solutions.

Want structured learning?

Take the full React course →