LearnThatStack Ace your next interview

Redux

Redux Toolkit 2 and React-Redux 9, with React Checked Oct 2026 68 code snippets

The model

  • One store holds the whole app state as a single object tree. Components read it through selectors and never change it directly.
  • To change state, you dispatch an action, a plain object with a type. The store runs the reducer, a pure function that returns the new state.
  • Reducers update immutably. They copy what changed, because React-Redux spots a change by comparing references with ===.
  • Redux Toolkit (RTK) is how you write Redux today. createSlice replaces type constants, action creators and switch statements.
  • Async work and other side effects run in middleware, such as thunks, listeners or RTK Query. A reducer never calls an API.
ComponentuseSelectorMiddlewarethunk, listenersReducerpure functionStoreone state treeDISPATCHdispatch(action)next(action)REDUCEcurrent statenew stateRENDERnotify subscribers
Read top to bottom. Every change takes this one path. Each useSelector then runs again, and its component re-renders only if the value changed.

Store, actions & reducers

the one-way loop every Redux app runs

The store

Read state, or run code after each dispatchstore.getState()store.subscribe(fn)
Change the statestore.dispatch(action)
Trace one state changeDispatch an action. The reducer returns new state, and the UI re-renders.

TipDispatching an action is the only way to change state. A standard app has one store.

Pure reducers

Write a reducer by hand(state, action) => newStateReturn the old state for any action it doesn't handle.
Update without mutating{ ...state, status: 'done' }
Keep a reducer pureNo API calls, timers or random values. Never change the arguments.

TipPure reducers are easy to test, and Redux DevTools can replay actions through them.

TrapMutating state keeps the same reference, so useSelector sees no change and nothing re-renders.

Redux Toolkit & slices

the standard way to write Redux today

Set up the store

Create the storeconfigureStore({ reducer })
Create it the old waycreateStore(reducer)Deprecated, but it still works.
List what configureStore addsThunk, DevTools, and dev checks for mutations and non-serializable values.

TipPass an object as reducer, and configureStore calls combineReducers for you.

Mutating code with Immer

stateprofileaddresstodosstate′profile′address′
Immer updateImmer copies each object on the path to city. todos is shared and keeps its reference.
Update a nested fieldstate.profile.address.city = city
Replace the whole statereturn action.payloadMutate the draft or return a value, never both.

TipThis works only inside createSlice or createReducer. In a plain reducer it really mutates the state.

Trapstate = newValue only rebinds the local variable, so it does nothing. Return the new value instead.

Connect React components

read the store and dispatch from components

Provider, useSelector and useDispatch

Give every component the store<Provider store={store}>
Read state in a componentuseSelector((s) => s.todos)
Dispatch from a componentuseDispatch()
Connect a class componentconnect(mapStateToProps)(TodoList)Deprecated since React-Redux 9.3, but it still works. New code uses the hooks.

TipProvider passes the store through context, not the state. Only components whose selection changed re-render.

Trapconnect skips a re-render when the props haven't changed. With hooks, wrap the component in React.memo for that.

Type the store and hooks

Infer RootState and AppDispatchReturnType<typeof store.getState>typeof store.dispatch
Make pre-typed hooks in React-Redux 9.1+useSelector.withTypes<RootState>()
Type an RTK Query endpointbuild.query<User[], number>({ query })

TipPut the typed hooks in their own file, like app/hooks.ts.

TrapThe plain Dispatch type doesn't know about thunks. Use AppDispatch to dispatch them.

Selectors & re-renders

read derived data without extra renders

Stop extra re-renders

Derive data and cache the resultcreateSelector([selectTodos], filterDone)It reruns only when an input changes.
Pass an argument to a selectorselectTodosByStatus(state, 'done')Add an input selector for the argument.
Select small valuesCall useSelector once per value, not for an object of several.
Return an object without extra rendersuseSelector(selectStats, shallowEqual)
Compare results your own wayuseSelector(selector, isEqual)

TipEvery dispatch runs every mounted selector. Pass IDs to list items, so each selects only its own record.

TrapA selector that calls filter or map returns a new array each time, so its component re-renders on every action.

Async logic & thunks

where API calls and other side effects go

Pick an async tool

Fetch and cache server dataRTK Query.
Run simple async logic with a thunk() => async (dispatch, getState) => {}
React to actions or run a long workflowThe listener middleware.
Cancel, debounce or race long flowsRedux Saga. The Redux docs advise against it for data fetching.

TipA thunk runs async code itself. A saga yields effects, plain objects that its middleware carries out.

TrapA thunk runs once and can't wait for other actions. Use a listener for that.

createAsyncThunk

Wrap an async API callcreateAsyncThunk('users/fetch', getUser)
Know the actions it dispatchespending, then fulfilled with the result or rejected with the error.
Reject with the server's error bodyrejectWithValue(err.response.data)
Catch a failure in a componentdispatch(fetchUser(id)).unwrap()Without unwrap, the promise always resolves.
Allow only valid status changesKeep a table of transitions. The reducer ignores any other event.

TipTrack one status field, like 'idle', 'loading' or 'failed', not separate booleans.

TrapWithout rejectWithValue, action.payload is undefined on failure. The error is in action.error.

Listener middleware

Create the middlewarecreateListenerMiddleware()
Run an effect after an actionstartListening({ actionCreator, effect })
Wait for a later actionlistenerApi.take(matcher, 5000)

TipPrepend it so it runs before the serializability check. Its own add and remove actions carry functions.

TraplistenerApi.condition resolves true for a failure action too. Use take to tell success from failure.

Races and optimistic updates

Ignore a stale responseKeep the newest requestId, and drop older results.
Cancel a running requestpromise.abort()Call it on the promise that dispatch returns.
Skip a fetch that's already runningReturn false from the thunk's condition option.

TipFor an optimistic update, dispatch the change first. If the request fails, dispatch a rollback.

TrapAn old failure must not undo a newer change. Track a version per item, or refetch on error.

Middleware & DevTools

extend dispatch, wrap the store, debug changes

Write middleware

Write a middleware(store) => (next) => (action) => next(action)
Add it and keep the defaultsgetDefaultMiddleware().concat(logger)
Say why the order mattersEach one sees only what the one before passes to next.
Cache API responsesCheck a cache slice first, and pass only misses to next.
Turn socket messages into actionsOpen the socket in a middleware, and dispatch an action per message.

TipA middleware can pass an action on, change it, delay it, stop it, or dispatch others.

TrapA dispatch inside a middleware runs the whole chain again, from the first middleware.

Enhancers and undo history

Write a store enhancer(createStore) => (reducer, preloaded) => store
Save the state encryptedLoad it as the preloaded state, save it in subscribe, and encrypt it with Web Crypto.
Add behaviour to any reducerWrap it in a higher-order reducer that handles some actions itself.
Keep undo history{ past: [], present, future: [] }A new action clears future.

TipMiddleware extends only dispatch. An enhancer can change the whole store, and applyMiddleware is one.

TrapReturn the old state when the wrapped reducer changed nothing, or every ignored action adds an undo step.

Redux DevTools and time travel

See every action and the state after itRedux DevTools lists them and can jump back to any earlier state.
Hide tokens from the recorded historydevTools: { actionSanitizer, stateSanitizer }

TipconfigureStore connects DevTools for you. Only createStore needs the extension set up by hand.

TrapNon-serializable values in state or actions, like promises, break time-travel debugging.

Fetch data with RTK Query

server data with caching built in

Define an API

Create the API slicecreateApi({ baseQuery, endpoints })
Add read and write endpointsbuild.query({ query: () => 'users' })build.mutation({ query })
Add it to the store[api.reducerPath]: api.reducerconcat(api.middleware)
Say how it differs from createAsyncThunkIt writes the reducers, loading state, caching and deduplication for you.

TrapWithout api.middleware, caching and refetching stop working.

Errors and response shapes

Tell a request error from a code error'status' in errorThen it's a FetchBaseQueryError.
Catch a failed mutationawait addUser(body).unwrap()
Reshape data before it's cachedtransformResponse: (res) => res.data

TipThe cache stores what transformResponse returns. It never runs for a queryFn endpoint.

TrapHooks don't throw on a failed request, so an error boundary never sees it. Read error, or rethrow it.

How the cache works

Keep unused data longerkeepUnusedDataFor: 300The default is 60 seconds.
Re-render only for the part you useselectFromResult
Read a cache entry outside a componentapi.endpoints.getUsers.select()Create it once, outside other functions.
Stream socket updates into the cacheonCacheEntryAdded(arg, { updateCachedData })

TipThe cache key is the endpoint plus its serialized arguments. A timestamp argument makes a new entry every time.

TrapThe cache isn't normalized. An item in two queries is stored twice, and tags keep them in sync.

Update the RTK Query cache

keep cached data correct after a write

Invalidate with tags

updateUserUser 5User LISTgetUser(5)getPostsgetUsers
updateUser(5)It invalidates two tags, so getUser(5) and getUsers refetch. getPosts keeps its cache.
Label what a query returnsprovidesTags: [{ type: 'User', id }]
Refetch after a mutationinvalidatesTags: ['User']

TipTag each item with its ID, and the collection with LIST. Invalidate LIST on create and delete.

Optimistic updates

Patch the cache before the server answersupdateQueryData('getPost', id, recipe)It's on api.util. Dispatch it in onQueryStarted.
Roll back if the request failspatchResult.undo()
Handle a conflicting editSend the version you edited. If the server's copy changed, keep one side, merge, or ask the user.
Send many changes in one requestPost them to a batch endpoint, then patch or invalidate each item.

TipWhen mutations can overlap, invalidating tags on error is safer than undoing each patch.

TrapupdateQueryData patches one existing entry. Patch the list and the detail view, or invalidate both.

Custom base queries and auth

Write a custom base queryasync (args, api, extraOptions) => ({ data })Return { data } or { error }. Never throw.
Add the token to every requestheaders.set('authorization', `Bearer ${token}`)
Refresh an expired token and retryWrap the base query. On a 401, refresh, store the token, and retry.

TipWhen several requests get a 401 at once, a mutex lets one refresh run while the others wait.

TrapKeep the refresh token in an httpOnly cookie, not in Redux. Any script on the page can read Redux state.

Test Redux code

check the resulting state, not the dispatched actions

Test slices, thunks and RTK Query

Test against a real storeconfigureStore({ reducer })Make a fresh one in each test.
Test a thunkDispatch it to a real store, mock HTTP with MSW, and check the state.
Record dispatched actionsstore.getActions()From redux-mock-store, which never runs reducers.
Test an RTK Query endpointMock the server with MSW 2, and run the hook with renderHook in a Provider.

TipTo test only how a slice stores a result, pass a hand-built fetchUser.fulfilled action to the reducer.

Trapredux-thunk 3 has no default export. Write import { thunk } from 'redux-thunk'.

Design a Redux app

what goes in the store, and when to skip Redux

What belongs in the store

Decide if state goes in ReduxPut it there if other parts of the app need it, or you want to cache or restore it.
Know what to keep out of the storePromises, functions and class instances.
Handle a formKeep the fields local or in React Hook Form. Dispatch once at the end.

TrapRedux Form kept every field in the store. Its own README now says to keep form state out of Redux.

Redux, Context or something else

Tell Redux from ContextContext passes a value down, and every consumer re-renders when it changes.
Tell Redux from MobXRedux changes are explicit actions. MobX mutates observables and re-renders on its own.
Hold server dataA data-fetching cache. TanStack Query, or RTK Query if the app uses Redux.

TipRedux is worth it when much of the app shares the same state. A small app can use Zustand or local state.

Structure a large app

Organize the codeBy feature, not by type. Each folder holds a slice and its API code.
Load a slice with its feature's coderootReducer.inject(featureSlice)rootReducer comes from combineSlices.
Create the store in Next.jsOnce per request, in a client StoreProvider. Server Components never touch it.

TipSplit RTK Query endpoints the same way, with api.injectEndpoints.

TrapA Next.js server handles many requests at once. A module-level store would be shared by all of them.

Auth and feature flags

Hold the sessionAn auth slice with the user and permissions. logout clears it all.
Gate a feature behind a flaguseSelector((s) => s.featureFlags[name])

TipLoad the flags before you render the parts they gate, so a feature doesn't flash on and off.

TrapUI checks only hide controls. The server must still enforce every permission and flag that protects data.

Found this useful? Pass it on.
Pro · $10/mo

This sheet stays free. With Pro, you can:

  • Every answer on this sheet. Read the full answer to all 84 questions it links, not just the 16 free ones.
  • My sheet. The cards you star and the ones you miss in Quiz me, from every sheet, on one page to quiz and print.
  • PDFs. Download this sheet and My sheet, laid out for A4, with every question a link to its answer.
  • The rest of Pro. Every answer in the question library, and 1,000 AI credits a month.