Suggest an editImprove this articleRefine the answer for “Redux toolkit”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Redux Toolkit** is the official toolset for writing Redux logic with far less boilerplate. `createSlice` handles action creators and Immer-based reducers in one call. `configureStore` adds Thunk and DevTools automatically. ```javascript const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value += 1 } // No spread needed } }); ``` **Key point:** write `state.value += 1` instead of `return { ...state, value: state.value + 1 }`.Shown above the full answer for quick recall.Answer (EN)Image**Redux Toolkit (RTK)** is the official toolset from Redux maintainers that cuts boilerplate by roughly 80% through Immer-powered reducers, auto-generated action creators, and a pre-configured store setup. ## Theory ### TL;DR - Vanilla Redux is writing every action type, switch case, and spread operator by hand. RTK generates all of that from a single `createSlice` config. - Main difference: `createSlice` + Immer lets you write `state.value += 1` directly instead of returning `{ ...state, value: state.value + 1 }`. - `configureStore` ships with Redux Thunk and DevTools enabled by default. - `createAsyncThunk` auto-generates `pending/fulfilled/rejected` action types for any async operation. - Decision rule: use RTK for all new Redux apps. Vanilla Redux only makes sense on legacy codebases that cannot migrate. ### Quick example ```javascript import { createSlice, configureStore } from '@reduxjs/toolkit'; const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value += 1; }, // Immer handles immutability decrement: (state) => { state.value -= 1; }, incrementByAmount: (state, action) => { state.value += action.payload; } } }); export const { increment, decrement, incrementByAmount } = counterSlice.actions; const store = configureStore({ reducer: { counter: counterSlice.reducer } }); ``` That one `createSlice` call replaces 40+ lines of vanilla Redux: no `ACTION_TYPE` constants, no switch cases, no spread operators. `configureStore` connects Thunk middleware and Redux DevTools automatically. ### Key difference from vanilla Redux In vanilla Redux, reducers must return a new state object explicitly: `{ ...state, value: state.value + 1 }`. RTK wraps reducers in Immer's `produce` function, which creates a Proxy around the state draft. You mutate the draft, Immer detects which paths changed, and produces a new immutable object through structural sharing. Only the changed parts get copied; everything else shares references. The result is identical to hand-written immutable code, but the syntax is far simpler. ### When to use - Small app with local UI state: skip Redux entirely. Zustand or Context is enough. - Medium React app with shared async data: RTK with slices and `createAsyncThunk`. - Complex app with normalized entities like users and posts: RTK plus `createEntityAdapter`. - Legacy Redux codebase: wrap existing reducers in `createSlice` incrementally, replace `createStore` with `configureStore`. - Non-React projects: RTK core utilities work in plain JS and Node.js. ### Comparison table | Feature | Vanilla Redux | Redux Toolkit | |---|---|---| | Reducer setup | Manual switch + spread operators | `createSlice` + Immer mutations | | Store config | `createStore` + manual middleware | `configureStore` (Thunk + DevTools by default) | | Async logic | Manual thunk boilerplate | `createAsyncThunk` (auto pending/fulfilled/rejected) | | Entity management | Manual normalization code | `createEntityAdapter` with built-in CRUD ops | | Action creators | Manually written | Auto-generated from `createSlice` | | Bundle size | ~2KB core | ~10KB gzipped (includes Immer + Thunk) | | When to use | Legacy code, custom middleware experiments | All new production apps (official recommendation) | ### How Immer works inside RTK When `createSlice` runs your reducer, it wraps it in Immer's `produce`. Immer creates a Proxy (draft) over the current state. Any mutation you write, like `state.items.push(item)`, gets intercepted by that Proxy and recorded. After your reducer returns, Immer walks the recorded changes and builds a new state object through structural sharing: only the changed paths get new references, unchanged paths share the original. This is why `state.value += 1` inside a slice is safe, and why the same mutation outside Immer would break Redux's change detection. `configureStore` composes the Redux store with `applyMiddleware(thunk)` and the Redux DevTools enhancer. You can still add custom middleware through the `middleware` option. ### Async operations with createAsyncThunk ```javascript import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; export const fetchTodos = createAsyncThunk('todos/fetch', async () => { const res = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5'); return res.json(); // Becomes action.payload in fulfilled }); const todosSlice = createSlice({ name: 'todos', initialState: { items: [], status: 'idle' }, reducers: { toggleTodo: (state, action) => { state.items[action.payload].completed = !state.items[action.payload].completed; } }, extraReducers: (builder) => { builder .addCase(fetchTodos.pending, (state) => { state.status = 'loading'; }) .addCase(fetchTodos.fulfilled, (state, action) => { state.items = action.payload; state.status = 'succeeded'; }) .addCase(fetchTodos.rejected, (state) => { state.status = 'failed'; }); } }); ``` `extraReducers` handles action types that originate outside the slice, like a `createAsyncThunk` response. The `reducers` field auto-generates action creators. `extraReducers` does not. That distinction trips people up in interviews. ### Common mistakes **Mutating state outside a slice** ```javascript // Wrong: direct mutation in a plain reducer const reducer = (state = { list: [] }, action) => { state.list.push(action.payload); // Breaks Redux change detection return state; }; ``` Immer only protects you inside `createSlice`. `useSelector` compares state references. If you mutate the existing object and return it, the reference does not change and components do not re-render. Fix: move logic into `createSlice`. **Missing `extraReducers` for async thunks** ```javascript // Wrong: thunk exists but slice ignores it const slice = createSlice({ name: 'todos', initialState: { items: [], status: 'idle' }, reducers: {}, extraReducers: {} // Empty - status stays 'idle' forever }); ``` If you dispatch `fetchTodos()` but have no `addCase(fetchTodos.fulfilled, ...)`, state never updates. Use the builder pattern and handle all three cases. **Empty reducer map in `configureStore`** ```javascript // Wrong configureStore({ reducer: {} }); // Store exists but holds nothing ``` Pass actual slice reducers: `reducer: { counter: counterSlice.reducer }`. **Skipping error handling for rejected thunks** If a fetch fails and you have no `.addCase(fetchTodos.rejected, ...)`, the app quietly stays in `loading` state. In production, always handle `rejected` with at least a status update. **Forgetting `upsertOne` after optimistic updates** When adding a post optimistically with a temp ID and the real server response arrives, you need `postsAdapter.upsertOne(state, action.payload)` to replace the temp entry. Without it you end up with both the temp and the real record in state at the same time. ### Real-world usage - React/Next.js dashboards: separate slices for users, orders, products. Each feature gets its own slice file. - RTK Query: API caching in apps already on Redux. Replaces manual fetch + thunk + loading state patterns without adding a second library. - Normalized feeds: `createEntityAdapter` for Twitter-like timelines where the same user object appears in multiple places. - SSR with Next.js: server-side store hydration in `getServerSideProps`, slice state serialized as JSON and sent to the client. - Migration path: most teams replace `createStore` with `configureStore` first, then convert reducers to slices feature by feature. ### Follow-up questions **Q:** What is Immer and why does RTK use it? **A:** Immer wraps your reducer in a Proxy that intercepts mutations. After the reducer runs, Immer produces a new immutable state through structural sharing. RTK uses it so you can write `state.value += 1` instead of `return { ...state, value: state.value + 1 }`. **Q:** What is the difference between `reducers` and `extraReducers` in `createSlice`? **A:** `reducers` creates both the reducer logic and the action creators automatically. `extraReducers` adds reducer logic for actions that originate outside the slice, like thunks or actions from other slices. No action creators are generated in `extraReducers`. **Q:** How does `createAsyncThunk` differ from a manually written thunk? **A:** It auto-generates three action types: `pending`, `fulfilled`, and `rejected`. It also wraps the async function in a try/catch. A manual thunk requires you to dispatch each of those states yourself and handle errors explicitly. **Q:** What is `createEntityAdapter` and when does it help? **A:** It converts an array like `[{id: 1, name: 'Alice'}]` into a normalized structure `{ ids: [1], entities: { 1: {id: 1, name: 'Alice'} } }` and provides CRUD operations like `addOne`, `upsertOne`, `removeOne`. It helps when the same entity appears in multiple parts of state and you need a single source of truth. **Q:** (Senior) How does tag invalidation work in RTK Query? **A:** Each query endpoint defines `providesTags` and each mutation defines `invalidatesTags`. When a mutation succeeds, RTK Query checks which cached queries share those tags and re-fetches them automatically. For optimistic updates, use `patchQueryData` to update the cache before the API responds, then undo the patch if the mutation rejects. ## Examples ### Counter slice: RTK vs vanilla Redux Same outcome, drastically different code volume. ```javascript // RTK: complete working slice in ~10 lines import { createSlice } from '@reduxjs/toolkit'; const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value += 1; } } }); console.log( counterSlice.reducer(undefined, counterSlice.actions.increment()) ); // { value: 1 } ``` Vanilla Redux equivalent requires: a constant for the action type, a separate action creator function, a reducer with a switch/case, and a spread to avoid mutation. RTK collapses all four into the `reducers` field. ### Todo app with async fetch A realistic pattern: fetch from an API, track loading state, allow local toggles. ```javascript import { createSlice, createAsyncThunk, configureStore } from '@reduxjs/toolkit'; export const fetchTodos = createAsyncThunk('todos/fetch', async () => { const res = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5'); return res.json(); }); const todosSlice = createSlice({ name: 'todos', initialState: { items: [], status: 'idle' }, reducers: { toggleTodo: (state, action) => { const todo = state.items[action.payload]; if (todo) todo.completed = !todo.completed; // Safe Immer mutation } }, extraReducers: (builder) => { builder .addCase(fetchTodos.pending, (state) => { state.status = 'loading'; }) .addCase(fetchTodos.fulfilled, (state, action) => { state.items = action.payload; state.status = 'succeeded'; }) .addCase(fetchTodos.rejected, (state) => { state.status = 'failed'; }); } }); const store = configureStore({ reducer: { todos: todosSlice.reducer } }); // In a React component: // const { items, status } = useSelector((state) => state.todos); // const dispatch = useDispatch(); // dispatch(fetchTodos()); // Triggers pending -> fulfilled lifecycle ``` After `fetchTodos()` resolves, `status` becomes `'succeeded'` and `items` holds the fetched data. `toggleTodo` works without spread operators because Immer tracks the mutation automatically. ### Entity adapter with optimistic updates This pattern trips up senior candidates. The key detail is `upsertOne` after the API responds. ```javascript import { createSlice, createEntityAdapter, createAsyncThunk } from '@reduxjs/toolkit'; const postsAdapter = createEntityAdapter(); // Produces: { ids: [], entities: {} } export const addNewPost = createAsyncThunk('posts/add', async (postData) => { const res = await fetch('/api/posts', { method: 'POST', body: JSON.stringify(postData) }); return res.json(); // Returns post with real server-assigned ID }); const postsSlice = createSlice({ name: 'posts', initialState: postsAdapter.getInitialState(), reducers: {}, extraReducers: (builder) => { builder .addCase(addNewPost.pending, (state, action) => { // Optimistic: add locally with a temp ID before API responds postsAdapter.addOne(state, { id: 'temp-' + Date.now(), ...action.meta.arg }); }) .addCase(addNewPost.fulfilled, (state, action) => { // Replace temp entry with real server response // upsertOne matches on id - without it, temp duplicates the real record postsAdapter.upsertOne(state, action.payload); }) .addCase(addNewPost.rejected, (state, action) => { // Remove the optimistic entry if the request failed const tempId = Object.keys(state.entities).find((id) => id.startsWith('temp-')); if (tempId) postsAdapter.removeOne(state, tempId); }); } }); // Selectors come for free: // postsAdapter.getSelectors((state) => state.posts).selectAll(store.getState()) ``` The `pending` case adds the post immediately so the UI feels instant. The `fulfilled` case calls `upsertOne`, which finds the existing entry by `id` and replaces it, or inserts a new one if no match. Skipping `upsertOne` and using `addOne` instead leaves the temp entry alongside the real one.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.