Back to Blog
React
October 7, 2025
13 min read
Edison Nkemande

State Management Patterns in Modern Applications

Explore state management approaches including Redux, Zustand, Context API, and when to use each pattern.

Introduction

State management is crucial for building scalable applications. This guide explores different patterns and their trade-offs.

State Management Options

Context API + useReducer

Lightweight, built-in solution for React:

import { createContext, useReducer } from 'react';
 
type State = { count: number };
type Action = { type: 'INCREMENT' | 'DECREMENT' };
 
const AppContext = createContext<[State, React.Dispatch<Action>] | null>(null);
 
export function AppProvider({ children }) {
  const [state, dispatch] = useReducer(
    (state: State, action: Action): State => {
      switch (action.type) {
        case 'INCREMENT':
          return { count: state.count + 1 };
        case 'DECREMENT':
          return { count: state.count - 1 };
        default:
          return state;
      }
    },
    { count: 0 }
  );
 
  return (
    <AppContext.Provider value={[state, dispatch]}>
      {children}
    </AppContext.Provider>
  );
}

Zustand

Lightweight and performant store:

import { create } from 'zustand';
 
interface Store {
  count: number;
  increment: () => void;
  decrement: () => void;
}
 
export const useStore = create<Store>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}));
 
// Usage
function Counter() {
  const { count, increment } = useStore();
  return <button onClick={increment}>{count}</button>;
}

Redux

For complex applications with predictable state flow:

import { createSlice, configureStore } from '@reduxjs/toolkit';
 
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value++; },
    decrement: (state) => { state.value--; },
  },
});
 
const store = configureStore({
  reducer: { counter: counterSlice.reducer },
});
 
// Usage
import { useSelector, useDispatch } from 'react-redux';
 
function Counter() {
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();
  
  return <button onClick={() => dispatch(counterSlice.actions.increment())}>{count}</button>;
}

Comparison

| Approach | Complexity | Performance | Bundle Size | Use Case | |----------|-----------|-------------|------------|----------| | useState | Low | Good | 0 | Simple state | | Context + useReducer | Medium | Fair | 0 | Medium apps | | Zustand | Low | Excellent | Small | Modern apps | | Redux | High | Excellent | Medium | Complex apps |

State Management Best Practices

1. Normalize State Structure

// ✓ Good: Normalized
const state = {
  users: {
    byId: { '1': { id: '1', name: 'John' } },
    allIds: ['1']
  }
};
 
// ✗ Bad: Nested
const state = {
  users: [{ id: '1', name: 'John', posts: [...] }]
};

2. Separate Concerns

const useUserStore = create(...); // User data
const useUIStore = create(...);   // UI state
const useAuthStore = create(...); // Authentication

3. Memoization for Performance

const selectUserById = (id: string) =>
  (state) => state.users.byId[id];
 
// Components only re-render if selected value changes
const user = useStore(selectUserById(userId));

Server State vs Client State

// Client State (local)
const [isModalOpen, setIsModalOpen] = useState(false);
 
// Server State (remote)
const { data: users } = useQuery(['users'], fetchUsers);

Use separate libraries for each:

  • Client: Zustand, Redux
  • Server: React Query, SWR

Conclusion

Choose state management based on application complexity. Start simple and evolve as your needs grow. Modern approaches like Zustand offer great developer experience with minimal overhead.

Share this article:

Related Articles