Back to React
React
easy
mid

What is the difference between Redux and Redux Toolkit?

Redux is the original predictable state container — verbose: hand-write action types, action creators, reducers, immutable updates. Redux Toolkit (RTK) is the official, batteries-included wrapper: `createSlice` autogenerates actions+reducers, Immer lets you 'mutate' state in reducers, `configureStore` adds DevTools + thunk by default, RTK Query handles server state. Use RTK — plain Redux is legacy.

7 min read·~5 min to think through

Redux Toolkit (RTK) is what the Redux team now recommends as the default way to write Redux. Plain Redux is not deprecated, just verbose.

Plain Redux

js
// action types
const INCREMENT = 'counter/increment';
const DECREMENT = 'counter/decrement';

// action creators
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });

// reducer
function counter(state = { value: 0 }, action) {
  switch (action.type) {
    case INCREMENT: return { ...state, value: state.value + 1 };
    case DECREMENT: return { ...state, value: state.value - 1 };
    default: return state;
  }
}

// store
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';

const store = createStore(
  combineReducers({ counter }),
  composeWithDevTools(applyMiddleware(thunk)),
);

A lot of boilerplate to add one number.

Redux Toolkit

ts
import { createSlice, configureStore } from '@reduxjs/toolkit';

const counter = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: state => { state.value++; },   // Immer makes 'mutation' safe
    decrement: state => { state.value--; },
  },
});

export const { increment, decrement } = counter.actions;

export const store = configureStore({
  reducer: { counter: counter.reducer },
});
// DevTools + thunk middleware enabled by default

That's the whole thing.

What RTK gives you

  1. createSlice — autogenerates action types and creators from reducer keys.
  2. Immer baked in — write 'mutating' code; under the hood it produces an immutable new state.
  3. configureStore — wires DevTools, thunk middleware, serializability checks automatically.
  4. createAsyncThunk — standard pattern for async actions with pending/fulfilled/rejected.
  5. RTK Query — full data-fetching + caching layer (alternative to React Query if you're already in Redux).
  6. TypeScript-first — types flow from your reducers to your dispatch/useSelector without manual typing.

Async with createAsyncThunk

ts
export const fetchUser = createAsyncThunk(
  'user/fetch',
  async (id: string) => (await fetch(`/api/user/${id}`)).json(),
);

const user = createSlice({
  name: 'user',
  initialState: { data: null, loading: false },
  reducers: {},
  extraReducers: builder => {
    builder
      .addCase(fetchUser.pending,   s => { s.loading = true; })
      .addCase(fetchUser.fulfilled, (s, a) => { s.loading = false; s.data = a.payload; });
  },
});

RTK Query (cache + fetch)

ts
const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: b => ({
    getUser: b.query<User, string>({ query: id => `user/${id}` }),
  }),
});

const { useGetUserQuery } = api;

When to use Redux at all

Modern default for server state is React Query / SWR. For client state, Context+useReducer or Zustand often suffice. Reach for Redux/RTK when:

  • The team already knows Redux.
  • You want time-travel debugging (DevTools).
  • App has complex cross-cutting client state (logged-in user, theme, complex undo/redo).
  • You want RTK Query as a single solution for state + data.

Summary

  • Don't write plain Redux in new code. Use RTK.
  • Don't use Redux at all for what React Query handles better (server state).
  • Use RTK when you've genuinely got cross-cutting client state worth a store.

Follow-up questions

  • When would you pick Zustand over Redux Toolkit?
  • How does Immer make 'mutation' safe?
  • What's the difference between createAsyncThunk and RTK Query?

Common mistakes

  • Writing plain Redux in 2025 — RTK is the recommended path.
  • Using Redux for server state when React Query/SWR fits better.
  • Storing non-serializable values (Date, Map, class instances) in Redux state — breaks devtools and rehydration.

Performance considerations

  • Selectors with `useSelector` re-run on every store change — keep them light or use `createSelector` (reselect) to memoize. Subscribe components to the smallest slice they need to limit re-renders.

Edge cases

  • Immer can be slow on very large state trees — flatten or use plain returns where it matters.
  • RTK Query's cache lifetime is global; you may need invalidation tags.
  • Migrating from plain Redux: `createSlice` per existing reducer is the path of least resistance.

Real-world examples

  • Pretty much every Redux codebase shipped after 2021 uses RTK. Big examples: Airbnb, Robinhood, Shopify admin (mixed). New projects rarely choose plain Redux.

Senior engineer discussion

Senior framing: Redux was teaching us about predictable state. RTK keeps the lesson and drops the boilerplate. The deeper question now is whether you need a global store at all — for many React apps the answer is 'server state goes to React Query, the rest is component state'.

Related questions