Back to React
React
medium
mid

How would you implement Redux Toolkit in a React application?

RTK collapses Redux boilerplate: `createSlice` (reducers + actions in one place with Immer), `configureStore` (middleware + devtools wired), `createAsyncThunk` (async lifecycle), `createEntityAdapter` (normalized CRUD), RTK Query (React-Query-like data layer over Redux). Reduces boilerplate ~70% vs hand-rolled Redux.

5 min read·~12 min to think through

Pieces

createSlice

ts
const cartSlice = createSlice({
  name: 'cart',
  initialState: { items: [] as Item[] },
  reducers: {
    add(state, action: PayloadAction<Item>) {
      state.items.push(action.payload);          // looks mutable — Immer makes it immutable under the hood
    },
    remove(state, action: PayloadAction<string>) {
      state.items = state.items.filter((i) => i.id !== action.payload);
    },
  },
});

export const { add, remove } = cartSlice.actions;
export default cartSlice.reducer;
  • Auto-generates action creators.
  • Reducer accepts mutable-looking code (Immer produces a new immutable state).
  • Action types are namespaced (cart/add).

configureStore

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

export const store = configureStore({
  reducer: { cart: cartSlice.reducer, profile: profileSlice.reducer },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Sets up:

  • Middleware (thunk, immutability check, serializability check).
  • Redux DevTools.
  • Hot-reloading-friendly setup.

Typed hooks

ts
import { useDispatch, useSelector } from 'react-redux';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

createAsyncThunk

ts
export const fetchProfile = createAsyncThunk('profile/fetch', async (id: string) => {
  const res = await api.profile(id);
  return res.data;
});

const slice = createSlice({
  name: 'profile',
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchProfile.pending, (s) => { s.status = 'loading'; })
      .addCase(fetchProfile.fulfilled, (s, a) => { s.status = 'done'; s.data = a.payload; })
      .addCase(fetchProfile.rejected, (s, a) => { s.status = 'error'; s.error = a.error.message; });
  },
});

Three action types per thunk: pending, fulfilled, rejected. Pattern for async lifecycle.

createEntityAdapter

ts
const adapter = createEntityAdapter<Post>({ sortComparer: (a, b) => b.createdAt - a.createdAt });
const initialState = adapter.getInitialState({ status: 'idle' });

const slice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    added: adapter.addOne,
    upserted: adapter.upsertOne,
    removed: adapter.removeOne,
  },
});

// Selectors
const { selectAll, selectById } = adapter.getSelectors((state: RootState) => state.posts);

Normalized state ({ ids: [], entities: {} }) with CRUD reducers — saves a lot of boilerplate.

RTK Query

ts
export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Post'],
  endpoints: (build) => ({
    getPosts: build.query<Post[], void>({
      query: () => 'posts',
      providesTags: ['Post'],
    }),
    addPost: build.mutation<Post, NewPost>({
      query: (body) => ({ url: 'posts', method: 'POST', body }),
      invalidatesTags: ['Post'],
    }),
  }),
});

export const { useGetPostsQuery, useAddPostMutation } = api;

A full data-fetching layer with caching, invalidation tags, optimistic updates, polling, RTK-DevTools integration.

Why people use it

  • Less boilerplate than hand-rolled Redux.
  • Built-in DevTools, middleware, types.
  • RTK Query competes with React Query inside the Redux ecosystem.
  • Strict action discipline still preserved.

Vs hooks/Zustand

  • RTK has more ceremony but more structure.
  • For large teams or complex client domains, the discipline is worth it.
  • For most app shapes, hooks + React Query + Zustand is lighter.

Interview framing

"createSlice defines state + reducers + actions in one place; Immer makes the mutable-looking code produce immutable state. configureStore wires middleware (thunk, immutability check) and DevTools. createAsyncThunk standardizes async lifecycle into pending/fulfilled/rejected. createEntityAdapter normalizes collections with CRUD reducers + selectors. RTK Query is a full data layer over Redux — caches with tag-based invalidation, like React Query but in the Redux store. RTK collapses about 70% of Redux boilerplate and is the recommended way to use Redux today."

Follow-up questions

  • Compare RTK Query and React Query.
  • When would you use createEntityAdapter?
  • What's the role of Immer in createSlice?

Common mistakes

  • Mixing legacy Redux with RTK in one app.
  • Mutating state outside reducers.
  • Using RTK Query and React Query side by side (pick one).

Performance considerations

  • Selector memoization matters; reselect or RTK's createSelector helps.

Edge cases

  • Non-serializable values in actions (Dates, class instances).
  • Listener middleware vs sagas.
  • RTK Query cache eviction tuning.

Real-world examples

  • Most enterprise React apps using Redux today.

Senior engineer discussion

Seniors choose RTK over hand-rolled Redux unequivocally and pick between RTK Query / React Query based on the rest of the stack.

Related questions