React State Management in 2025 - Choosing the Right Tool

When to Use What (And Why Redux Might Not Be the Answer)

“Should I use Redux?” is the wrong question. The right question is: “What problem am I actually solving?”

The State Management Panic

Three months into React, a senior dev asked: “Why aren’t you using Redux?”

I panicked. Added Redux. My simple TODO app had actions, action creators, reducers, selectors, middleware, thunks.

500 lines of boilerplate for 3 pieces of state.

Most apps don’t need heavy state management.

The Ladder

Start simple. Climb only when you have to:

Level 5: Zustand/Jotai (Global, minimal)
Level 4: Redux Toolkit (Heavy global)
Level 3: Context + useReducer (Light global)
Level 2: Context API (Shared state)
Level 1: useState + props (Local state)

Start at Level 1. Move up only when you feel pain.

Level 1: useState + Props (80% of Cases)

When to use:

  • State is local to component
  • Only 2-3 components need the data
  • Data doesn’t change frequently
function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const addTodo = (text: string) => {
    setTodos([...todos, { id: Date.now(), text, done: false }]);
  };
  return (
    <div>
      <TodoInput onAdd={addTodo} />
      <TodoList todos={todos} />
    </div>
  );
}

This is fine. Don’t over-engineer.

Level 2: Context API

When to use:

  • Multiple components need same data
  • Data doesn’t update frequently
  • Tired of prop drilling

Perfect for: Theme, auth, configuration

Level 3: Context + useReducer

When to use:

  • State logic is complex
  • Multiple related state updates
  • Transitions need to be predictable

Benefits: Complex logic centralized, explicit transitions, easier to test.

Level 4: Redux Toolkit

When to use:

  • Large team needs predictable patterns
  • Complex state with many interactions
  • Need middleware

Don’t use if: App is small, you’re solo, no complex state.

Redux Toolkit fixes old Redux’s verbosity, but it’s still heavy.

Level 5: Zustand (My Current Favorite)

Why I love it:

  • Tiny (1KB)
  • No providers needed
  • No boilerplate
  • TypeScript-friendly
import create from 'zustand';

const useTodoStore = create<TodoStore>((set) => ({
  todos: [],
  addTodo: (text) => set((state) => ({
    todos: [...state.todos,
      { id: Date.now().toString(), text, done: false }]
  })),
  toggleTodo: (id) => set((state) => ({
    todos: state.todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    )
  }))
}));

// Usage
const todos = useTodoStore(state => state.todos);

Performance tip: Subscribe only to what you need.

Server State vs Client State

Don’t confuse them.

  • Client State: UI state, form inputs, theme
  • Server State: User data, posts, database records

For server state, use React Query or SWR:

import { useQuery } from '@tanstack/react-query';

function UserProfile({ userId }) {
  const { data: user } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json())
  });
  return <div>{user?.name}</div>;
}

Don’t use Redux/Zustand for server data. React Query/SWR handles: caching, background refetching, loading states, error handling.

The Decision Tree

Do you need global state?
├─ No → useState + props
└─ Yes
   └─ Is it server data?
      ├─ Yes → React Query / SWR
      └─ No → Is it complex logic?
         ├─ No → Context API
         └─ Yes → Zustand / Redux Toolkit

My Current Stack (2025)

Client state:

  • Local: useState
  • Shared (simple): Context API
  • Shared (complex): Zustand

Server state:

  • React Query

This handles 99% of apps.

Common Mistakes

1. Using Global State for Everything

// BAD
const useStore = create((set) => ({
  isModalOpen: false  // Only one component cares
}));

// GOOD
function MyComponent() {
  const [isModalOpen, setIsModalOpen] = useState(false);
}

Rule: If only one component cares, keep it local.

2. Storing Derived State

// BAD
const useStore = create((set) => ({
  items: [],
  totalPrice: 0  // Can get out of sync!
}));

// GOOD
function Cart() {
  const items = useStore(state => state.items);
  const totalPrice = items.reduce((sum, item) =>
    sum + item.price, 0
  );
}

The best state management solution is the simplest one that solves your problem.

Most apps are fine with useState, Context, and React Query.

Start simple. Scale when you actually need to.