What Redux is for
TipRedux suits state that many distant components read and update.
type. The store runs the reducer, a pure function that returns the new state.===.createSlice replaces type constants, action creators and switch statements.useSelector then runs again, and its component re-renders only if the value changed.TipRedux suits state that many distant components read and update.
store.getState() store.subscribe(fn) store.dispatch(action) TipDispatching an action is the only way to change state. A standard app has one store.
{ type: 'todos/added', payload: todo }todoAdded(todo)TipAn action creator is a function that returns the action. createSlice writes them for you.
(state, action) => newStateReturn the old state for any action it doesn't handle.{ ...state, status: 'done' }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.
configureStore({ reducer })createStore(reducer) Deprecated, but it still works.TipPass an object as reducer, and configureStore calls combineReducers for you.
createSlice({ name, initialState, reducers })reducer, and an actions object with one action creator per reducer.TipEach reducer key becomes an action type like 'todos/. You write no constants and no switch.
state.profile.address.city = cityreturn 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.
prepare: (text) => ({ payload: { id: nanoid(), text } })builder.addCase(fetchUser.fulfilled, fn)TipMake IDs and timestamps in prepare, never in the reducer, which must stay pure.
TrapRTK 2.0 removed the object form of extraReducers. Use the builder callback.
<Provider store={store}>useSelector((s) => s.todos)useDispatch()connect(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.
ReturnType<typeof store.getState>typeof store.dispatchuseSelector.withTypes<RootState>() build.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.
Asked in interviews
AskedHow do you type the Redux store and hooks in TypeScript?createSelector([selectTodos], filterDone)It reruns only when an input changes.selectTodosByStatus(state, 'done')Add an input selector for the argument.useSelector once per value, not for an object of several.useSelector(selectStats, shallowEqual)useSelector(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.
{ ids: [], entities: {} }Other items refer to them by ID.createEntityAdapter() Reducers like addOne, selectors like selectById.TipWith normalized state, an update changes one record in one place. Join related items back in memoized selectors.
() => async (dispatch, getState) => {}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('users/fetch', getUser)pending, then fulfilled with the result or rejected with the error.rejectWithValue(err.response.data) dispatch(fetchUser(id)).unwrap() Without unwrap, the promise always resolves.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.
createListenerMiddleware() startListening({ actionCreator, effect })listenerApi.take(matcher, 5000)TipPrepend it so it runs before the serializability check. Its own add and remove actions carry functions.
Traplistener resolves true for a failure action too. Use take to tell success from failure.
requestId, and drop older results.promise.abort()Call it on the promise that dispatch returns.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.
(store) => (next) => (action) => next(action)getDefaultMiddleware().concat(logger) next.next.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.
(createStore) => (reducer, preloaded) => storesubscribe, and encrypt it with Web Crypto.{ 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.
devTools: { 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.
createApi({ baseQuery, endpoints })build.query({ query: () => 'users' })build.mutation({ query })[api.reducerPath]: api.reducerconcat(api.middleware) TrapWithout api.middleware, caching and refetching stop working.
useGetUsersQuery() isLoading is true only on the first load. isFetching is true for any request.const [addUser] = useAddUserMutation() useGetUserQuery(id ?? skipToken)TipA lazy hook like use sends nothing until you call its trigger.
'status' in errorThen it's a FetchBaseQueryError .await addUser(body).unwrap() transformResponse: (res) => res.dataTipThe cache stores what transform 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.
build.infiniteQuery(options) New in RTK 2.6.formData.append('file', file)Send the FormData as the body.TipFor plain pages, pass the page number as the argument. Each page gets its own cache entry.
TrapDon't set Content-Type for FormData. The browser adds the multipart boundary itself.
keepUnusedDataFor: 300The default is 60 seconds.selectFromResult api.endpoints.getUsers.select() Create it once, outside other functions.onCacheEntryAdded(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.
Asked in interviews
AskedHow does caching work in RTK Query?providesTags: [{ type: 'User', id }]invalidatesTags: ['User']TipTag each item with its ID, and the collection with LIST. Invalidate LIST on create and delete.
Asked in interviews
AskedHow do you implement RTK Query cache invalidation strategies?updateQueryData('getPost', id, recipe)It's on api.util. Dispatch it in onQueryStarted.patchResult.undo() 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.
Asked in interviews
AskedHow do you handle optimistic updates with RTK Query?async (args, api, extraOptions) => ({ data })Return { data } or { error }. Never throw.headers.set('authorization', `Bearer ${token}`)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.
Asked in interviews
AskedHow do you implement RTK Query with authentication?configureStore({ reducer })Make a fresh one in each test.store.getActions() From redux-mock-store, which never runs reducers.renderHook in a Provider.TipTo test only how a slice stores a result, pass a hand-built fetch action to the reducer.
Trapredux-thunk 3 has no default export. Write import { thunk } from 'redux-thunk'.
TrapRedux Form kept every field in the store. Its own README now says to keep form state out of Redux.
TipRedux is worth it when much of the app shares the same state. A small app can use Zustand or local state.
rootReducer.inject(featureSlice) rootReducer comes from combineSlices.StoreProvider. Server Components never touch it.TipSplit RTK Query endpoints the same way, with api..
TrapA Next.js server handles many requests at once. A module-level store would be shared by all of them.
logout clears it all.useSelector((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.
Nothing matches. Try a shorter word, or clear the search.
With Pro, you can:
Already have Pro? Sign in