React State Management in Depth #5: Redux Toolkit and the Legacy Context

5 min read

The Zustand and Jotai we’ve covered so far are relatively recent, lightweight tools. The Redux of this post is the tool that ruled the era before them. For a while, “global state in React = Redux” was practically the formula, and countless codebases built in that era still carry it today. So even though there’s less occasion to write it anew, there’s still plenty of occasion to read and maintain it.

This post covers it not in old Redux’s verbose code but in its current officially recommended form, Redux Toolkit (RTK).

Redux’s core concepts #

Redux stands on three rules. The model itself shares the same root as React’s built-in useReducer.

  • A single store — the app’s state gathers into one big object.
  • Changed only through actions — you don’t mutate state directly; you dispatch an action that describes “what happened.”
  • A reducer computes the next state — a pure function (current state, action) => next state takes responsibility for the change.

This strictness is both Redux’s strength and its cost. Because every state change goes through an action, tracing and debugging (including time-travel debugging) are powerful. But there are correspondingly more steps to go through.

Why old Redux was verbose #

In the past, adding a single feature to Redux meant hand-writing action type constants, action creator functions, and a reducer, each in a different file. A single counter took dozens of lines, and this boilerplate was the biggest reason people left Redux. Redux Toolkit is the tool the official team built precisely to solve this problem.

Installation and a slice #

Install
npm install @reduxjs/toolkit react-redux

RTK’s centerpiece is createSlice. It defines state, reducers, and actions in one place.

features/counterSlice.js
import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment: (state) => {
      state.value += 1; // looks like a direct mutation, but it's safe (explained below)
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;

You see code like state.value += 1 that looks like a direct mutation. Reducers are supposed to treat state as immutable, but RTK keeps Immer inside and converts this “mutating-looking” code into a safe immutable update. That removes the chore of copying objects with the spread operator. And the action creators matching the reducer names (increment, etc.) are generated automatically. Most of old Redux’s boilerplate disappears here.

Configuring the store and the Provider #

You gather several slices into one store.

store.js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./features/counterSlice";

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

And unlike #3 Zustand and #4 Jotai, Redux must wrap the tree in a Provider.

main.jsx
import { Provider } from "react-redux";
import { store } from "./store";

createRoot(document.getElementById("root")).render(
  <Provider store={store}>
    <App />
  </Provider>
);

Reading and writing in a component #

You use useSelector to read state and useDispatch to send actions.

Counter.jsx
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement, incrementByAmount } from "./features/counterSlice";

function Counter() {
  const value = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div>
      <button onClick={() => dispatch(decrement())}>-</button>
      <span>{value}</span>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
    </div>
  );
}

The selector in useSelector shares the same idea as #3 Zustand’s selector. The component re-renders only when the returned value changes. Because actions must go through dispatch, when and why state changed is recorded action by action in the Redux DevTools. This traceability is why Redux is still chosen for large teams and complex state.

Note
Redux also includes a server-state tool, RTK Query, which plays a role similar to #2’s TanStack Query. For a project already deep into Redux, RTK Query is the natural choice; otherwise, TanStack Query is lighter. Either way, the key is the principle from #1: leave server state to a dedicated tool, and keep only client state in your ordinary slices.

Where Redux stands in a new project #

Redux Toolkit is far more concise than old Redux, but it still has more structure to go through than Zustand or Jotai. Slices, store configuration, and a Provider are required by default. So when choosing a tool, you can sort it out like this.

SituationFit
Maintaining a codebase already written in ReduxRTK is the natural modernization path
Large team, where strict change conventions and traceability matterRedux’s strengths shine
Small app, only lightweight global state neededZustand / Jotai are lighter
State is mostly server dataTanStack Query-centric, global store minimized

In other words, for a small new project there’s less reason to reach for Redux first, but since much of the code in the field is built on Redux, being able to read and handle it is still important. That’s why this post is called “the legacy context.”

Wrapping up #

Redux Toolkit is the officially recommended form that strips away old Redux’s boilerplate.

  • createSlice — state, reducers, and actions in one place; safe “mutating-looking” updates via Immer
  • configureStore — a single store gathering the slices
  • Provider — Redux must wrap the tree (a difference from Zustand and Jotai)
  • useSelector / useDispatch — reading and dispatching actions
  • the strength is traceability and convention; the cost is the weight of structure

We’ve now seen the main options, from built-in tools through TanStack Query, Zustand, Jotai, and Redux Toolkit. In the final post, “React State Management in Depth #6: Which Tool, When — a Decision Guide,” we tie all of this into a single decision flow for what to reach for in real situations.

X