Local useState and Context work for many apps. As complexity grows, dedicated state libraries provide better performance and developer experience.

When Context Isn’t Enough

  • Frequent updates cause wide re-renders
  • Deep component trees with prop drilling
  • Complex state logic across many features
  • Need for devtools and middleware

Minimal API, no boilerplate:

  npm install zustand
  
  // store/useCartStore.js
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export const useCartStore = create(
    persist(
        (set, get) => ({
            items: [],
            addItem: (product) => set((state) => {
                const existing = state.items.find(i => i.id === product.id);
                if (existing) {
                    return {
                        items: state.items.map(i =>
                            i.id === product.id
                                ? { ...i, quantity: i.quantity + 1 }
                                : i
                        )
                    };
                }
                return { items: [...state.items, { ...product, quantity: 1 }] };
            }),
            removeItem: (id) => set((state) => ({
                items: state.items.filter(i => i.id !== id)
            })),
            clearCart: () => set({ items: [] }),
            total: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0)
        }),
        { name: 'cart-storage' }
    )
);
  

Usage in components:

  function CartButton() {
    const itemCount = useCartStore(state =>
        state.items.reduce((sum, i) => sum + i.quantity, 0)
    );
    return <button>Cart ({itemCount})</button>;
}

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

Select only the state you need — components re-render only when that slice changes.

Redux Toolkit (Large Apps)

Structured, predictable, excellent devtools:

  npm install @reduxjs/toolkit react-redux
  
  // store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
    name: 'counter',
    initialState: { value: 0 },
    reducers: {
        increment: (state) => { state.value += 1; },
        decrement: (state) => { state.value -= 1; },
        incrementBy: (state, action) => { state.value += action.payload; }
    }
});

export const { increment, decrement, incrementBy } = counterSlice.actions;
export default counterSlice.reducer;
  
  import { useSelector, useDispatch } from 'react-redux';
import { increment } from './store/counterSlice';

function Counter() {
    const count = useSelector(state => state.counter.value);
    const dispatch = useDispatch();
    return <button onClick={() => dispatch(increment())}>{count}</button>;
}
  

TanStack Query (Server State)

Separate server state from client state:

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

function UserList() {
    const { data, isLoading, error } = useQuery({
        queryKey: ['users'],
        queryFn: () => fetch('/api/users').then(r => r.json())
    });

    if (isLoading) return <p>Loading...</p>;
    if (error) return <p>Error: {error.message}</p>;
    return data.map(user => <div key={user.id}>{user.name}</div>);
}

function CreateUser() {
    const queryClient = useQueryClient();
    const mutation = useMutation({
        mutationFn: (newUser) =>
            fetch('/api/users', {
                method: 'POST',
                body: JSON.stringify(newUser)
            }).then(r => r.json()),
        onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] })
    });

    return (
        <button onClick={() => mutation.mutate({ name: 'New User' })}>
            Add User
        </button>
    );
}
  

Choosing a Solution

Tool Best for
useState + Context Small apps, theme, auth
Zustand Medium apps, cart, UI state
Redux Toolkit Large teams, complex client state
TanStack Query API data, caching, sync
Jotai/Recoil Atomic fine-grained state

Use TanStack Query for server data and Zustand for client UI state — a powerful combination for most React apps.