Skip to main content
โšก Calmops

State Management: Redux, Zustand, Context API

State Management: Redux, Zustand, Context API

State management is crucial for scalable React applications. This article covers popular state management solutions.

Introduction

State management provides:

  • Centralized state
  • Predictable updates
  • Time-travel debugging
  • Middleware support
  • Scalability

Understanding state management helps you:

  • Manage complex state
  • Share state across components
  • Debug state changes
  • Scale applications
  • Improve performance

Context API

Basic Context Setup

import { createContext, useContext, useState } from 'react';

// โœ… Good: Create context
const AppContext = createContext();

// โœ… Good: Provider component
function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);

  const login = (userData) => {
    setUser(userData);
  };

  const logout = () => {
    setUser(null);
  };

  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light');
  };

  const addNotification = (message) => {
    setNotifications([...notifications, message]);
  };

  const value = {
    user,
    login,
    logout,
    theme,
    toggleTheme,
    notifications,
    addNotification
  };

  return (
    <AppContext.Provider value={value}>
      {children}
    </AppContext.Provider>
  );
}

// โœ… Good: Custom hook for context
function useApp() {
  const context = useContext(AppContext);
  if (!context) {
    throw new Error('useApp must be used within AppProvider');
  }
  return context;
}

// Usage
function App() {
  return (
    <AppProvider>
      <Dashboard />
    </AppProvider>
  );
}

function Dashboard() {
  const { user, logout, theme } = useApp();

  return (
    <div className={`theme-${theme}`}>
      <h1>Welcome, {user?.name}</h1>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

Advanced Context Patterns

// โœ… Good: Separate contexts for different concerns
const AuthContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  const login = async (email, password) => {
    setLoading(true);
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ email, password })
      });
      const data = await response.json();
      setUser(data);
    } finally {
      setLoading(false);
    }
  };

  return (
    <AuthContext.Provider value={{ user, login, loading }}>
      {children}
    </AuthContext.Provider>
  );
}

function useAuth() {
  return useContext(AuthContext);
}

// โœ… Good: Reducer pattern with context
const initialState = {
  items: [],
  loading: false,
  error: null
};

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.payload]
      };
    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload)
      };
    case 'SET_LOADING':
      return { ...state, loading: action.payload };
    case 'SET_ERROR':
      return { ...state, error: action.payload };
    default:
      return state;
  }
}

const CartContext = createContext();

function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, initialState);

  const addItem = (item) => {
    dispatch({ type: 'ADD_ITEM', payload: item });
  };

  const removeItem = (itemId) => {
    dispatch({ type: 'REMOVE_ITEM', payload: itemId });
  };

  return (
    <CartContext.Provider value={{ state, addItem, removeItem }}>
      {children}
    </CartContext.Provider>
  );
}

function useCart() {
  return useContext(CartContext);
}

Redux

Redux Setup

import { createStore, combineReducers } from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';

// โœ… Good: Action types
const ACTIONS = {
  LOGIN: 'LOGIN',
  LOGOUT: 'LOGOUT',
  SET_LOADING: 'SET_LOADING',
  SET_ERROR: 'SET_ERROR'
};

// โœ… Good: Action creators
const login = (user) => ({
  type: ACTIONS.LOGIN,
  payload: user
});

const logout = () => ({
  type: ACTIONS.LOGOUT
});

const setLoading = (loading) => ({
  type: ACTIONS.SET_LOADING,
  payload: loading
});

// โœ… Good: Reducers
const authReducer = (state = { user: null, loading: false }, action) => {
  switch (action.type) {
    case ACTIONS.LOGIN:
      return { ...state, user: action.payload };
    case ACTIONS.LOGOUT:
      return { ...state, user: null };
    case ACTIONS.SET_LOADING:
      return { ...state, loading: action.payload };
    default:
      return state;
  }
};

const themeReducer = (state = 'light', action) => {
  switch (action.type) {
    case 'TOGGLE_THEME':
      return state === 'light' ? 'dark' : 'light';
    default:
      return state;
  }
};

// โœ… Good: Combine reducers
const rootReducer = combineReducers({
  auth: authReducer,
  theme: themeReducer
});

// โœ… Good: Create store
const store = createStore(rootReducer);

// โœ… Good: Setup app
function App() {
  return (
    <Provider store={store}>
      <Dashboard />
    </Provider>
  );
}

// โœ… Good: Use Redux in components
function Dashboard() {
  const dispatch = useDispatch();
  const { user, loading } = useSelector(state => state.auth);
  const theme = useSelector(state => state.theme);

  const handleLogin = async (email, password) => {
    dispatch(setLoading(true));
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ email, password })
      });
      const data = await response.json();
      dispatch(login(data));
    } finally {
      dispatch(setLoading(false));
    }
  };

  return (
    <div className={`theme-${theme}`}>
      {loading ? (
        <p>Loading...</p>
      ) : user ? (
        <h1>Welcome, {user.name}</h1>
      ) : (
        <button onClick={() => handleLogin('[email protected]', 'password')}>
          Login
        </button>
      )}
    </div>
  );
}

Redux Middleware

// โœ… Good: Custom middleware
const loggerMiddleware = store => next => action => {
  console.log('Dispatching:', action);
  const result = next(action);
  console.log('New state:', store.getState());
  return result;
};

// โœ… Good: Async middleware
const asyncMiddleware = store => next => action => {
  if (typeof action === 'function') {
    return action(store.dispatch, store.getState);
  }
  return next(action);
};

// โœ… Good: Create store with middleware
import { applyMiddleware } from 'redux';

const store = createStore(
  rootReducer,
  applyMiddleware(loggerMiddleware, asyncMiddleware)
);

// โœ… Good: Async action creators
const loginAsync = (email, password) => async (dispatch) => {
  dispatch(setLoading(true));
  try {
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ email, password })
    });
    const data = await response.json();
    dispatch(login(data));
  } catch (error) {
    dispatch({ type: 'SET_ERROR', payload: error.message });
  } finally {
    dispatch(setLoading(false));
  }
};

Zustand

Basic Zustand Setup

import { create } from 'zustand';

// โœ… Good: Create store
const useAuthStore = create((set) => ({
  user: null,
  loading: false,
  error: null,

  login: async (email, password) => {
    set({ loading: true });
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ email, password })
      });
      const data = await response.json();
      set({ user: data, error: null });
    } catch (error) {
      set({ error: error.message });
    } finally {
      set({ loading: false });
    }
  },

  logout: () => {
    set({ user: null });
  },

  setError: (error) => {
    set({ error });
  }
}));

// โœ… Good: Use store in components
function Dashboard() {
  const { user, loading, login, logout } = useAuthStore();

  return (
    <div>
      {loading ? (
        <p>Loading...</p>
      ) : user ? (
        <>
          <h1>Welcome, {user.name}</h1>
          <button onClick={logout}>Logout</button>
        </>
      ) : (
        <button onClick={() => login('[email protected]', 'password')}>
          Login
        </button>
      )}
    </div>
  );
}

// โœ… Good: Multiple stores
const useThemeStore = create((set) => ({
  theme: 'light',
  toggleTheme: () => set((state) => ({
    theme: state.theme === 'light' ? 'dark' : 'light'
  }))
}));

const useNotificationStore = create((set) => ({
  notifications: [],
  addNotification: (message) => set((state) => ({
    notifications: [...state.notifications, message]
  })),
  removeNotification: (id) => set((state) => ({
    notifications: state.notifications.filter(n => n.id !== id)
  }))
}));

Advanced Zustand Patterns

// โœ… Good: Selectors for performance
const useAuthStore = create((set) => ({
  user: null,
  email: '',
  // ...
}));

// Selectors
const selectUser = (state) => state.user;
const selectEmail = (state) => state.email;

function Component() {
  const user = useAuthStore(selectUser);
  const email = useAuthStore(selectEmail);
  // Only re-renders when user or email changes
}

// โœ… Good: Middleware
const useStore = create(
  (set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 }))
  }),
  (set) => ({
    // Middleware
    set: (state, action) => {
      console.log('State update:', action);
      set(state);
    }
  })
);

// โœ… Good: Persist middleware
import { persist } from 'zustand/middleware';

const useAuthStore = create(
  persist(
    (set) => ({
      user: null,
      login: (user) => set({ user }),
      logout: () => set({ user: null })
    }),
    {
      name: 'auth-storage'
    }
  )
);

// โœ… Good: Immer middleware
import { immer } from 'zustand/middleware/immer';

const useStore = create(
  immer((set) => ({
    todos: [],
    addTodo: (text) => set((state) => {
      state.todos.push({ id: Date.now(), text });
    })
  }))
);

Comparison

Context API vs Redux vs Zustand

// Context API: Simple, built-in, good for small apps
// Pros: No dependencies, simple API
// Cons: Can cause unnecessary re-renders, verbose

// Redux: Powerful, predictable, great for large apps
// Pros: Time-travel debugging, middleware, large ecosystem
// Cons: Boilerplate, steep learning curve

// Zustand: Simple, lightweight, good for medium apps
// Pros: Minimal boilerplate, good performance, TypeScript support
// Cons: Smaller ecosystem, less mature

// โœ… Good: Choose based on needs
// Small app: Context API
// Medium app: Zustand
// Large app: Redux

Best Practices

  1. Keep state normalized:

    // โœ… Good: Normalized state
    const state = {
      users: {
        1: { id: 1, name: 'John' },
        2: { id: 2, name: 'Jane' }
      },
      userIds: [1, 2]
    };
    
    // โŒ Bad: Nested state
    const state = {
      users: [
        { id: 1, name: 'John', posts: [...] },
        { id: 2, name: 'Jane', posts: [...] }
      ]
    };
    
  2. Use selectors:

    // โœ… Good: Selectors prevent re-renders
    const selectUserName = (state) => state.user.name;
    const name = useAuthStore(selectUserName);
    
    // โŒ Bad: Entire state
    const { user } = useAuthStore();
    
  3. Avoid deeply nested state:

    // โœ… Good: Flat structure
    const state = {
      user: { id: 1, name: 'John' },
      theme: 'light'
    };
    
    // โŒ Bad: Deeply nested
    const state = {
      app: {
        ui: {
          theme: {
            current: 'light'
          }
        }
      }
    };
    

Summary

State management is essential. Key takeaways:

  • Use Context API for simple apps
  • Use Zustand for medium apps
  • Use Redux for large apps
  • Keep state normalized
  • Use selectors for performance
  • Avoid prop drilling
  • Choose based on complexity

Next Steps

Comments