Skip to content

React · State Management

Redux Toolkit in React — Complete Interview Guide

7 min read Updated 2026-06-24 Share:

Practice Redux Toolkit interview questions

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 — useState or useReducer is 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.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel