Back to the blog

Zustand vs Redux: State Management in React

Comparing Zustand and Redux for global state management: store, actions, middleware, persist and best practices.

January 5, 2026
11 min read
723 views
MOLOTILO

MOLOTILO DIGITAL

Zustand vs Redux: State Management in React

Why you need state management

As an app grows, passing state through props becomes inconvenient (prop drilling). State management libraries solve this by providing a global store.

Zustand — simplicity and minimalism

Zustand is a lightweight library (1KB) with a simple API:

import { create } from 'zustand';

// Store definition
interface CartStore {
  items: CartItem[];
  total: number;
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  clearCart: () => void;
}

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

  addItem: (item) => set((state) => {
    const existingItem = state.items.find(i => i.id === item.id);

    if (existingItem) {
      return {
        items: state.items.map(i =>
          i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
        ),
        total: state.total + item.price
      };
    }

    return {
      items: [...state.items, { ...item, quantity: 1 }],
      total: state.total + item.price
    };
  }),

  removeItem: (id) => set((state) => {
    const item = state.items.find(i => i.id === id);
    return {
      items: state.items.filter(i => i.id !== id),
      total: state.total - (item ? item.price * item.quantity : 0)
    };
  }),

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

// Usage in components
function CartButton() {
  const itemCount = useCartStore((state) => state.items.length);
  return <button>Cart ({itemCount})</button>;
}

function CartTotal() {
  const total = useCartStore((state) => state.total);
  return <div>Total: {total}₽</div>;
}

function AddToCartButton({ product }: { product: Product }) {
  const addItem = useCartStore((state) => state.addItem);
  return <button onClick={() => addItem(product)}>Add to Cart</button>;
}

Zustand Middleware

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

// Persist — saving to localStorage
const useCartStore = create<CartStore>()(
  devtools(
    persist(
      (set) => ({
        items: [],
        total: 0,
        addItem: (item) => set((state) => ({ ... })),
      }),
      {
        name: 'cart-storage', // key in localStorage
        partialize: (state) => ({ items: state.items }), // what to save
      }
    ),
    { name: 'CartStore' } // name in DevTools
  )
);

// Immer middleware for immutable updates
import { immer } from 'zustand/middleware/immer';

const useStore = create<State>()(
  immer((set) => ({
    users: [],
    addUser: (user) => set((state) => {
      state.users.push(user); // Mutation is safe with immer
    }),
  }))
);

Redux Toolkit — the industry standard

Redux Toolkit simplifies working with Redux:

import { createSlice, configureStore, PayloadAction } from '@reduxjs/toolkit';

// Slice — a part of the state
const cartSlice = createSlice({
  name: 'cart',
  initialState: {
    items: [] as CartItem[],
    total: 0
  },
  reducers: {
    addItem: (state, action: PayloadAction<CartItem>) => {
      const existingItem = state.items.find(i => i.id === action.payload.id);

      if (existingItem) {
        existingItem.quantity += 1;
      } else {
        state.items.push({ ...action.payload, quantity: 1 });
      }

      state.total += action.payload.price;
    },
    removeItem: (state, action: PayloadAction<string>) => {
      const index = state.items.findIndex(i => i.id === action.payload);
      if (index !== -1) {
        state.total -= state.items[index].price * state.items[index].quantity;
        state.items.splice(index, 1);
      }
    },
    clearCart: (state) => {
      state.items = [];
      state.total = 0;
    }
  }
});

export const { addItem, removeItem, clearCart } = cartSlice.actions;

// Store
const store = configureStore({
  reducer: {
    cart: cartSlice.reducer,
    // other slices
  }
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

// Typed hooks
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

// Usage
function CartButton() {
  const itemCount = useAppSelector((state) => state.cart.items.length);
  return <button>Cart ({itemCount})</button>;
}

function AddToCartButton({ product }: { product: Product }) {
  const dispatch = useAppDispatch();
  return (
    <button onClick={() => dispatch(addItem(product))}>
      Add to Cart
    </button>
  );
}

Redux Async Actions (RTK Query)

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

const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Products', 'Cart'],
  endpoints: (builder) => ({
    getProducts: builder.query<Product[], void>({
      query: () => 'products',
      providesTags: ['Products']
    }),
    addToCart: builder.mutation<Cart, { productId: string }>({
      query: (body) => ({
        url: 'cart',
        method: 'POST',
        body
      }),
      invalidatesTags: ['Cart']
    })
  })
});

export const { useGetProductsQuery, useAddToCartMutation } = api;

// Usage
function ProductList() {
  const { data: products, isLoading, error } = useGetProductsQuery();
  const [addToCart] = useAddToCartMutation();

  if (isLoading) return <Spinner />;

  return (
    <ul>
      {products?.map(product => (
        <li key={product.id}>
          {product.name}
          <button onClick={() => addToCart({ productId: product.id })}>
            Add
          </button>
        </li>
      ))}
    </ul>
  );
}

Comparing Zustand and Redux

CriterionZustandRedux Toolkit
Size~1KB~10KB
BoilerplateMinimalMore
DevToolsVia middlewareBuilt in
AsyncSimple, in actionsRTK Query / Thunk
EcosystemGrowingHuge
For a projectSmall/mediumAny size

Conclusion

Zustand is a great choice for small and medium projects thanks to its simplicity. Redux Toolkit suits large applications with a team, where DevTools and the ecosystem matter. Both are type-safe and work well with TypeScript.

Start with Zustand for simplicity. Move to Redux when you need RTK Query, complex middleware or a large team.

Enjoyed the article?

Subscribe to our blog so you don’t miss new posts