Redux has a reputation for boilerplate. Three files for a single counter — action type constants, action creator functions, and a hand-written reducer with spread operators everywhere — made plain Redux painful to maintain. Redux Toolkit (RTK) is the official answer to that problem. It ships as the recommended way to write Redux code, and it is what interviewers expect you to reach for today. Understanding what RTK removes, what it adds, and when it is (and isn't) the right tool is the fastest way to impress in a senior React interview.
Setting Up the Store
Everything starts with configureStore. It replaces the old createStore
call and adds two things automatically: Redux DevTools Extension support in
development, and the redux-thunk middleware for async action creators.
// store/store.ts
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
import userReducer from '../features/user/userSlice'
export const store = configureStore({
reducer: {
counter: counterReducer, // state.counter
user: userReducer, // state.user
},
// middleware and devTools are wired automatically — override only when needed
})
// Export inferred types for TypeScript consumers
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
Wrap your app in <Provider store={store}> from react-redux. In TypeScript
projects, create a hooks.ts file that exports typed useAppSelector and
useAppDispatch wrappers so components get full autocomplete without repeating
type annotations everywhere.
Slices — The Core Primitive
A slice is a self-contained unit of Redux state. createSlice takes a
name, an initial state, and a reducers object, and returns a reducer function
plus auto-generated action creators. Under the hood it wraps every reducer in
Immer, so you write code that looks like mutation but produces a new
immutable state.
// features/counter/counterSlice.ts
import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter', // prefixes action type strings: 'counter/increment'
initialState: { value: 0 },
reducers: {
increment(state) {
state.value++ // Immer draft — no spread needed
},
decrement(state) {
state.value--
},
incrementBy(state, action) {
state.value += action.payload // payload is the argument passed to the creator
},
},
})
export const { increment, decrement, incrementBy } = counterSlice.actions
export default counterSlice.reducer // plug into configureStore
The Immer integration is the most important productivity win in RTK. Deeply nested updates — which required nested spread objects in plain Redux — become straightforward property assignments on the draft. The one constraint: you must either mutate the draft or return a replacement value, never both.
In components, read state with useSelector and write with useDispatch:
const count = useSelector(state => state.counter.value)
const dispatch = useDispatch()
dispatch(incrementBy(5))
For derived values — filtered lists, aggregates, anything that builds a new
array or object — reach for createSelector from RTK. It memoizes the
result and prevents unnecessary re-renders when the underlying data hasn't
actually changed.
Async Logic with createAsyncThunk
Fetching data is where plain Redux got especially painful. RTK's answer is
createAsyncThunk: you give it an action type prefix and an async function
that returns the payload. It automatically dispatches pending, fulfilled,
and rejected actions around your async call.
// features/user/userThunks.ts
import { createAsyncThunk } from '@reduxjs/toolkit'
export const fetchUser = createAsyncThunk('user/fetchById', async (userId: number) => {
const res = await fetch(`/api/users/${userId}`)
if (!res.ok) throw new Error('User not found') // throw to trigger rejected
return res.json() // return value becomes action.payload
})
Handle the lifecycle in extraReducers using the builder pattern:
extraReducers(builder) {
builder
.addCase(fetchUser.pending, (state) => {
state.status = 'loading'
state.error = null
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.status = 'succeeded'
state.data = action.payload
})
.addCase(fetchUser.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message ?? 'Unknown error'
})
}
A critical convention: model loading state as a string ('idle' | 'loading' | 'succeeded' | 'failed') rather than a boolean. A boolean cannot represent all
four states without impossible combinations like isLoading: true and
isError: true simultaneously.
In components, dispatch the thunk and call .unwrap() if you need to handle
errors locally:
try {
const user = await dispatch(fetchUser(42)).unwrap()
navigate(`/profile/${user.id}`)
} catch (err) {
setFormError(err.message)
}
RTK Query for Server State
For data that lives on a server — lists, single items, paginated results —
createAsyncThunk still requires you to write every slice manually. RTK
Query is a complete data-fetching layer built on top of Redux that handles
caching, deduplication, background refetching, and cache invalidation for you.
Define an API with createApi:
// features/posts/postsApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const postsApi = createApi({
reducerPath: 'postsApi', // key in Redux state
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Post'],
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Post'], // cache tagged so mutations can bust it
}),
createPost: builder.mutation({
query: (body) => ({ url: '/posts', method: 'POST', body }),
invalidatesTags: ['Post'], // refetch getPosts automatically on success
}),
}),
})
export const { useGetPostsQuery, useCreatePostMutation } = postsApi
Add postsApi.reducer to configureStore's reducer map and
postsApi.middleware to the middleware chain — RTK Query uses middleware to
manage subscription lifetimes and cache expiry.
In components, hooks replace all the manual loading/error logic:
function PostsList() {
const { data: posts = [], isLoading, isError } = useGetPostsQuery()
if (isLoading) return <Spinner />
if (isError) return <p>Failed to load posts.</p>
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}
The tag-based cache invalidation is RTK Query's most interview-worthy feature. By tagging queries and invalidating those tags in mutations, you get automatic UI updates after writes without manually merging cache state.
When Redux Is (and Isn't) the Right Tool
This is the question senior interviewers most want to hear a nuanced answer to.
Use Redux when:
- State is shared across many unrelated components and the component tree is too deep for prop drilling or too broad for a single Context provider to be efficient.
- Update logic is complex — multiple actions can affect the same piece of state, or one action must update multiple slices.
- You need time-travel debugging, action replay, or strict audit logging of state transitions.
- The team is large enough that a single, predictable data flow helps coordination.
Skip Redux when:
- The state is local to one component or a small subtree —
useStateoruseReduceris simpler and keeps the logic next to the UI. - The state is entirely server-derived — RTK Query or React Query manages caching, deduplication, and synchronization better than a hand-written async slice ever will.
- The app is genuinely small — introducing slices, selectors, action creators, and a Provider for a to-do demo adds cognitive weight without any payoff.
A reliable smell: if your slice contains flags like isModalOpen or
activeTab, or if only one component dispatches and reads a given action, that
state belongs local.
What Interviewers Are Looking For
When the topic of Redux comes up in an interview, the weakest answer is reciting
the pattern. The strongest answer explains trade-offs: why RTK made Redux
practical, why RTK Query can replace most createAsyncThunk slices for server
state, and — crucially — why you would choose Zustand or plain Context for
simpler global state instead of reaching for Redux by default.
Expect to sketch createSlice with extraReducers from memory, explain the
Immer contract (mutate the draft or return a replacement, not both), and describe
tag-based cache invalidation in RTK Query at a whiteboard level. Those three
topics cover the overwhelming majority of real RTK interview questions.