blog/8
Frontend Development8 min read

State Management in React: Redux vs Zustand vs Context

By Marin Cholakov11/05/2024
ReactState ManagementReduxZustandContext
State Management in React: Redux vs Zustand vs Context

Choosing the right state management solution is crucial for React applications. Let's compare the most popular options and understand when to use each:

React Context + useReducer

Best for: Small to medium apps, component tree state sharing

// UserContext.js
const UserContext = createContext();

const userReducer = (state, action) => {
  switch (action.type) {
    case 'SET_USER':
      return { ...state, user: action.payload };
    case 'SET_LOADING':
      return { ...state, loading: action.payload };
    case 'SET_ERROR':
      return { ...state, error: action.payload };
    default:
      return state;
  }
};

export const UserProvider = ({ children }) => {
  const [state, dispatch] = useReducer(userReducer, {
    user: null,
    loading: false,
    error: null
  });

  const login = async (credentials) => {
    dispatch({ type: 'SET_LOADING', payload: true });
    try {
      const user = await authAPI.login(credentials);
      dispatch({ type: 'SET_USER', payload: user });
    } catch (error) {
      dispatch({ type: 'SET_ERROR', payload: error.message });
    } finally {
      dispatch({ type: 'SET_LOADING', payload: false });
    }
  };

  return (
    <UserContext.Provider value={{ ...state, login }}>
      {children}
    </UserContext.Provider>
  );
};

// Usage
const Profile = () => {
  const { user, loading, login } = useContext(UserContext);
  
  if (loading) return <div>Loading...</div>;
  
  return <div>Welcome, {user?.name}</div>;
};

Redux Toolkit

Best for: Large applications, complex state logic, time-travel debugging

// store/userSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

export const loginUser = createAsyncThunk(
  'user/login',
  async (credentials, { rejectWithValue }) => {
    try {
      const response = await authAPI.login(credentials);
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data.message);
    }
  }
);

const userSlice = createSlice({
  name: 'user',
  initialState: {
    user: null,
    loading: false,
    error: null
  },
  reducers: {
    clearError: (state) => {
      state.error = null;
    },
    logout: (state) => {
      state.user = null;
    }
  },
  extraReducers: (builder) => {
    builder
      .addCase(loginUser.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(loginUser.fulfilled, (state, action) => {
        state.loading = false;
        state.user = action.payload;
      })
      .addCase(loginUser.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      });
  }
});

export const { clearError, logout } = userSlice.actions;
export default userSlice.reducer;

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

export const store = configureStore({
  reducer: {
    user: userReducer
  }
});

// Usage
import { useSelector, useDispatch } from 'react-redux';

const Profile = () => {
  const { user, loading, error } = useSelector(state => state.user);
  const dispatch = useDispatch();

  const handleLogin = (credentials) => {
    dispatch(loginUser(credentials));
  };

  if (loading) return <div>Loading...</div>;
  
  return <div>Welcome, {user?.name}</div>;
};

Zustand

Best for: Simple global state, TypeScript-first apps, minimal boilerplate

// stores/userStore.js
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

const useUserStore = create(
  devtools(
    persist(
      (set, get) => ({
        user: null,
        loading: false,
        error: null,
        
        login: async (credentials) => {
          set({ loading: true, error: null });
          try {
            const user = await authAPI.login(credentials);
            set({ user, loading: false });
          } catch (error) {
            set({ error: error.message, loading: false });
          }
        },
        
        logout: () => set({ user: null }),
        
        clearError: () => set({ error: null }),
        
        // Computed values
        isAuthenticated: () => !!get().user,
      }),
      {
        name: 'user-storage',
        partialize: (state) => ({ user: state.user })
      }
    )
  )
);

export default useUserStore;

// Usage
const Profile = () => {
  const { user, loading, login, isAuthenticated } = useUserStore();
  
  // Selective subscription for performance
  const userOnly = useUserStore(state => state.user);
  
  if (loading) return <div>Loading...</div>;
  
  return <div>Welcome, {user?.name}</div>;
};

Comparison Matrix

Feature Context Redux Toolkit Zustand
Bundle Size 0KB ~8KB ~2KB
Learning Curve Low Medium Low
Boilerplate Medium Medium Minimal
DevTools Basic Excellent Good
TypeScript Manual Good Excellent
Persistence Manual Plugin Built-in
Performance Can cause re-renders Optimized Optimized
Async Actions Manual Built-in Manual

When to Use What?

Use React Context when:

  • Small to medium applications
  • State is mostly UI-related
  • Few components need global state
  • You want to avoid external dependencies

Use Redux Toolkit when:

  • Large, complex applications
  • Complex state logic and workflows
  • Need time-travel debugging
  • Team is familiar with Redux patterns
  • Need robust middleware ecosystem

Use Zustand when:

  • Want minimal boilerplate
  • TypeScript-first development
  • Simple global state needs
  • Performance is critical
  • Want modern, hooks-based API

Performance Tips

Context Optimization

// Split contexts to prevent unnecessary re-renders
const UserContext = createContext();
const UserActionsContext = createContext();

const UserProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const actions = useMemo(() => ({ setUser }), []);
  
  return (
    <UserActionsContext.Provider value={actions}>
      <UserContext.Provider value={user}>
        {children}
      </UserContext.Provider>
    </UserActionsContext.Provider>
  );
};

Zustand Selectors

// Only subscribe to specific state slices
const userName = useUserStore(state => state.user?.name);
const isLoading = useUserStore(state => state.loading);

Choose the solution that best fits your application's complexity and team expertise.

Share this post