Mastering Redux Toolkit: A Complete Beginner-to-Pro Guide (2025)
Mastering Redux Toolkit: A Complete Beginner-to-Pro Guide (2025)
If you’ve ever felt overwhelmed by Redux’s boilerplate or complexity, Redux Toolkit (RTK) is here to make your life easier. It’s the official, opinionated, and modern way to use Redux, eliminating most of the repetitive setup while enforcing best practices. Whether you’re a beginner or an experienced developer, this guide will help you understand Redux Toolkit step-by-step — from state management to dispatching actions and async logic.
🧠 What is Redux Toolkit?
Redux Toolkit (RTK) is the recommended way to write Redux logic. It builds on the core principles of Redux but abstracts away much of the boilerplate. With utilities like createSlice, configureStore, and createAsyncThunk, you can write cleaner, faster, and more predictable state management code.
✅ Why Use Redux Toolkit?
- Less boilerplate code.
- Built-in immutability using Immer.
- Simplified store configuration.
- Integrated async logic (via
createAsyncThunk). - Great DevTools support out of the box.
⚙️ Step 1: Project Setup
If you’re using React or Next.js, install the required dependencies:
npm install @reduxjs/toolkit react-reduxThen create a store.ts file (or .js):
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {}, // we'll add slices here later
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;Wrap your app with the <Provider> component so that Redux state is accessible everywhere:
import { Provider } from 'react-redux';
import { store } from './app/store';
function MyApp({ Component, pageProps }) {
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
}
export default MyApp;🧩 Step 2: Create a Slice
A slice is a part of the Redux state and logic dedicated to a specific feature.
Example: counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CounterState {
value: number;
}
const initialState: CounterState = { value: 0 };
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => { state.value += 1 },
decrement: (state) => { state.value -= 1 },
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;Add this slice to your store:
import counterReducer from '../features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});⚡ Step 3: Using Redux in Components
Use useSelector and useDispatch hooks from react-redux:
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState, AppDispatch } from '../app/store';
import { increment, decrement, incrementByAmount } from './counterSlice';
export function Counter() {
const count = useSelector((state: RootState) => state.counter.value);
const dispatch = useDispatch<AppDispatch>();
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
</div>
);
}🌐 Step 4: Async Logic with createAsyncThunk
Redux Toolkit simplifies async calls with createAsyncThunk. Here’s how you handle API calls:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
const response = await fetch('/api/users');
return await response.json();
});
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle', error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => { state.loading = 'pending'; })
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = 'succeeded';
state.entities = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = 'failed';
state.error = action.error.message;
});
},
});
export default usersSlice.reducer;This approach automatically handles async states (pending, fulfilled, rejected).
🎯 Step 5: Selectors and Derived State
Selectors help extract and compute values from state efficiently:
export const selectCount = (state: RootState) => state.counter.value;Use them with useSelector() for performance and cleaner code.
🧩 Step 6: Recommended Folder Structure
Organize your Redux code feature-wise:
src/
├── app/
│ ├── store.ts
├── features/
│ ├── counter/
│ │ ├── counterSlice.ts
│ │ ├── Counter.tsx
│ │ ├── counterSelectors.ts
│ │ ├── counterTypes.ts
Each slice has its own logic, types, and components — easier to scale and maintain.
🔥 Best Practices
✅ Keep your Redux state minimal and normalized. ✅ Avoid deeply nested state objects. ✅ Use RTK Query for advanced data fetching and caching. ✅ Group files by feature, not type. ✅ Write reusable selectors for derived data. ✅ Never mutate state directly (RTK uses Immer for immutability under the hood). ✅ Follow Redux Style Guide for consistency.
🧩 Step 7: Advanced Concepts
- Entity Adapters → Normalize large data collections.
- RTK Query → API caching and server-state sync.
- SSR with Next.js → Use
makeStore()pattern for SSR hydration. - Middleware → Handle side effects (logging, analytics, etc.).
- Testing → Unit test reducers and async thunks easily.
🧠 Core Redux Concepts Recap
| Concept | Description |
|---|---|
| Store | The centralized state container that holds all application state. |
| Slice | A logical piece of the store that bundles reducers, actions, and initial state. |
| Reducer | A pure function that updates the state based on actions. |
| Action | An event that describes what changed (type + payload). |
| Dispatch | Sends an action to the store to trigger state updates. |
| Selector | A function that extracts specific data from the store. |
| Thunk | Middleware function for async operations like API calls. |
| Middleware | Code that runs between dispatch and the reducer. |
🚀 Conclusion
Redux Toolkit is the modern, simplified evolution of Redux — combining power with productivity. By using slices, async thunks, and best practices, you’ll keep your codebase clean, scalable, and easy to debug.
Whether you’re building a small app or a full SaaS product, Redux Toolkit makes managing complex state predictable and efficient.
🧾 Further Reading
This pattern handles pending, fulfilled, and rejected states for the async process. ([Medium][2])
6. Selectors & derived state
- Create “selector” functions to encapsulate state lookup logic. Example:
selectCount = (state: RootState) => state.counter.value. - Use memoization (e.g.,
createSelector) if you compute derived data. Helps performance. ([redux.js.org][1]) - Keep your state shape normalized (especially for collections) to avoid nested and hard-to-update state. ([redux.js.org][1])
7. Structure & best practices
Given your stack and growth ambition, follow some structural and style-guidance things:
- Feature folder structure: each feature (slice) has its folder, e.g.,
/features/counter/withcounterSlice.ts,Counter.tsx,counterSelectors.ts,counterTypes.ts. - Avoid “God” reducers; keep each slice focused.
- Don’t store redundant/derived data in state – compute via selectors.
- Use naming conventions: slice name should reflect feature; action names should describe intent.
- Do not mutate state directly (RTK lets you write “mutative” code, but it’s still immutably updated under the hood). The style guide emphasises “Do Not Mutate State”. ([redux.js.org][3])
- Write tests for your slices (reducers, thunks) if you want maintainability.
- Consider entity-normalization for lists of objects (via
createEntityAdapterin RTK if needed). - Use logging, DevTools, keep actions clear for debugging.
- For SSR/Next.js, be aware of store rehydration, initial state, etc (if you need to integrate Redux with Next’s server-side logic).
8. Integration with Next.js & advanced scenarios
Since you mentioned Next.js:
- Use a single store, and ensure it works with SSR. For example, you might create a
makeStoreand wrap your_app.tsx. - Be mindful of asynchronous state loading (e.g., fetch on server vs client).
- Use RTK Query (a part of RTK) if you have heavy data-fetching/caching needs — it simplifies the fetching + caching layer. (Optional)
- Use middleware or enhancers if you need additional logging or persistence (Redux Persist, etc).
- For large apps, use code splitting and dynamically load reducers/slices as needed.
🧠 Core concepts summarised
Here’s a reference summary of the key concepts you should understand deeply:
- Store: the single source of truth for your app state. Created via
configureStore. - Slice: a feature-specific part of state + its reducers/actions. Created via
createSlice. - State: the data in your store. Should be minimal, normalized, and reflect UI/logic needs.
- Reducers: pure functions (sort of) that take state + action → new state. In RTK you can write simpler code thanks to Immer.
- Actions / Action creators: functions that dispatch a description of “what happened”. In RTK,
createSlicegenerates these for you. - Dispatch: the mechanism by which you send actions to the store (e.g., via
dispatch(increment())). Components typically useuseDispatch. - Selectors: functions that read (and optionally derive) state from the store — e.g.,
state.counter.value. - Async logic / Thunks: side-effects and asynchronous operations handled via
createAsyncThunkor custom middleware. - Middleware: extra logic in the dispatch chain (e.g., for async, logging, etc). RTK sets up good defaults.
- Immutable updates: Redux state should be treated as immutable — RTK and Immer make this easier.
- DevTools / debugging: Inspecting state, actions, time‐traveling, etc. RTK supports this out-of-box.
- Best practices / patterns: Good structure, naming, minimal state, normalized data, clear separation of concerns. The official Style Guide covers this. ([redux.js.org][3])