Redux Toolkit Cheat Sheet
Covers Redux Toolkit's configureStore, createSlice reducers, createAsyncThunk for async logic, React-Redux hooks, and RTK Query data fetching.
configureStore
The recommended way to set up a Redux store.
// store.jsimport { configureStore } from '@reduxjs/toolkit';import counterReducer from './counterSlice';export const store = configureStore({ reducer: { counter: counterReducer, }, // devtools + thunk middleware + dev-mode checks included by default});
createSlice
Actions and a reducer generated from one object.
// counterSlice.jsimport { createSlice } from '@reduxjs/toolkit';const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value += 1; // Immer lets you "mutate" state safely }, incrementByAmount: (state, action) => { state.value += action.payload; }, },});export const { increment, incrementByAmount } = counterSlice.actions;export default counterSlice.reducer;
createAsyncThunk
Dispatches pending/fulfilled/rejected automatically.
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';export const fetchUser = createAsyncThunk( 'user/fetchById', async (userId) => { const res = await fetch(`/api/users/${userId}`); return res.json(); });const userSlice = createSlice({ name: 'user', initialState: { data: null, status: 'idle' }, reducers: {}, extraReducers: (builder) => { builder .addCase(fetchUser.pending, (state) => { state.status = 'loading'; }) .addCase(fetchUser.fulfilled, (state, action) => { state.status = 'succeeded'; state.data = action.payload; }) .addCase(fetchUser.rejected, (state) => { state.status = 'failed'; }); },});
React-Redux Hooks & RTK Query
Read/dispatch state, then auto-generate data-fetching hooks.
// Reading/dispatching stateimport { useSelector, useDispatch } from 'react-redux';import { increment } from './counterSlice';function Counter() { const count = useSelector((state) => state.counter.value); const dispatch = useDispatch(); return <button onClick={() => dispatch(increment())}>{count}</button>;}// RTK Query: auto-generated hooks for data fetching + cachingimport { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';export const api = createApi({ reducerPath: 'api', baseQuery: fetchBaseQuery({ baseUrl: '/api' }), endpoints: (builder) => ({ getUsers: builder.query({ query: () => 'users' }), }),});export const { useGetUsersQuery } = api;// const { data, isLoading } = useGetUsersQuery();
Key Concepts
Terms you'll see throughout the RTK docs.
- createSlice- Generates action creators and a reducer from one object, using Immer internally
- configureStore- Sets up the store with sane defaults: thunk middleware, DevTools, dev-mode checks
- Immer- Library RTK uses so reducers can write mutating-looking code that produces immutable updates
- RTK Query- Built-in data-fetching/caching layer that generates hooks like useGetUsersQuery
- extraReducers- Where a slice responds to actions defined outside itself, e.g. thunk lifecycle actions
createEntityAdapter for Normalized State
Normalizes collections into a sorted id array plus an entities lookup, with generated CRUD reducers and selectors.
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';const usersAdapter = createEntityAdapter({ selectId: (user) => user.id, sortComparer: (a, b) => a.name.localeCompare(b.name),});const usersSlice = createSlice({ name: 'users', initialState: usersAdapter.getInitialState({ status: 'idle' }), reducers: { userAdded: usersAdapter.addOne, usersReceived: usersAdapter.setAll, userUpdated: usersAdapter.updateOne, // { id, changes: {...} } userRemoved: usersAdapter.removeOne, },});// Generated memoized selectorsexport const { selectAll: selectAllUsers, selectById: selectUserById, selectIds: selectUserIds,} = usersAdapter.getSelectors((state) => state.users);export const { userAdded, usersReceived, userUpdated, userRemoved } = usersSlice.actions;export default usersSlice.reducer;
createListenerMiddleware
The modern RTK replacement for hand-rolled side-effect middleware and redux-saga for simple cases.
import { createListenerMiddleware, isAnyOf } from '@reduxjs/toolkit';import { userUpdated, userRemoved } from './usersSlice';export const listenerMiddleware = createListenerMiddleware();listenerMiddleware.startListening({ matcher: isAnyOf(userUpdated, userRemoved), effect: async (action, listenerApi) => { listenerApi.cancelActiveListeners(); // debounce-like: drop stale runs await listenerApi.delay(300); const state = listenerApi.getState(); await fetch('/api/audit-log', { method: 'POST', body: JSON.stringify({ action, state }) }); },});// store.jsexport const store = configureStore({ reducer: rootReducer, middleware: (getDefault) => getDefault().prepend(listenerMiddleware.middleware),});
Memoized Selectors with createSelector
Re-exported from Reselect; avoids recomputation and unnecessary re-renders on unrelated state changes.
import { createSelector } from '@reduxjs/toolkit';const selectTodos = (state) => state.todos.items;const selectFilter = (state) => state.todos.filter;// Only recomputes when selectTodos or selectFilter's outputs changeexport const selectVisibleTodos = createSelector( [selectTodos, selectFilter], (todos, filter) => { switch (filter) { case 'completed': return todos.filter((t) => t.completed); case 'active': return todos.filter((t) => !t.completed); default: return todos; } });// In a component: useSelector(selectVisibleTodos) returns the SAME// array reference across renders when inputs haven't changed,// preventing children wrapped in React.memo from re-rendering.
Advanced RTK APIs
Less common but powerful tools once you outgrow basic slices.
- createEntityAdapter- Normalizes a collection into { ids, entities } with generated CRUD reducers and selectors
- createListenerMiddleware- Runs side effects in response to dispatched actions; built-in replacement for simple saga/observable use cases
- combineSlices- Combines slice reducers and supports lazy-loading feature slices injected after store creation
- createSelector- Reselect's memoized selector factory, re-exported from RTK for derived/computed state
- isAnyOf / isAllOf- Matcher utilities for listener middleware and extraReducers to react to multiple action types at once
- optimistic updates (onQueryStarted)- RTK Query mutation hook for updating the cache immediately, then rolling back on error
- serializableCheck / immutableCheck- Dev-only middleware checks configureStore enables by default to catch non-serializable state or accidental mutation
Optimistic Updates with onQueryStarted
Update the cache before the request resolves, then roll back on failure.
updatePost: builder.mutation({ query: ({ id, ...patch }) => ({ url: `posts/${id}`, method: 'PATCH', body: patch }), async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) { const patchResult = dispatch( api.util.updateQueryData('getPost', id, (draft) => { Object.assign(draft, patch); // Immer draft, mutate directly }) ); try { await queryFulfilled; } catch { patchResult.undo(); // roll back the optimistic change on error } },}),
Never both mutate the draft state AND return a new value from a createSlice reducer — Immer only allows one or the other, and mixing them throws "An immer producer returned a new value and modified its draft" at runtime.