Redux Toolkit: The Modern Way to Manage State in React
State management is a core challenge in building scalable React applications. While classic Redux has long been a popular solution, it’s often criticized for its boilerplate and complexity. Redux Toolkit (RTK) is the official, recommended approach for writing Redux logic today. It dramatically simplifies Redux code, encourages best practices, and integrates seamlessly with modern React.
Why Redux Toolkit?
Classic Redux requires a lot of setup: action types, action creators, reducers, and store configuration. This can be overwhelming for beginners and tedious for experienced developers. Redux Toolkit solves these problems by:
- Reducing boilerplate with sensible defaults and helper functions.
- Encouraging best practices (like using Immer for immutable updates).
- Providing utilities for common tasks (like async logic).
- Making Redux code easier to read, write, and maintain.
Key Concepts in Redux Toolkit
Let’s break down the main building blocks of Redux Toolkit, with code samples and explanations.
1. Slices
A slice is a collection of Redux reducer logic and actions for a single feature of your app, defined in one file. Slices are created using `createSlice`.
Example: Counter Slice
import { createSlice } from '@reduxjs/toolkit';
const initialState = { value: 0 };
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment(state) {
state.value++;
},
decrement(state) {
state.value--;
},
reset(state) {
state.value = 0;
},
incrementByAmount(state, action) {
state.value += Number(action.payload);
},
},
});
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;
export default counterSlice.reducer;What’s happening here?
- `createSlice` automatically generates action creators and action types.
- Reducer functions can “mutate” state directly, thanks to Immer (under the hood, state is still immutable).
- All logic for the counter feature is in one place.
2. Store
The store holds your application’s state and connects your slices. With Redux Toolkit, you use `configureStore` to set up the store with minimal configuration.
Example: Store Setup
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export default store;Key points:
- Each slice reducer is registered under a key (here, `counter`).
- You can add more slices as your app grows.
3. Actions and Reducers
Get Vinod Jatav’s stories in your inbox
Join Medium for free to get updates from this writer.
Actions and reducers are defined together in the slice. When you call an action creator (like `increment()`), Redux dispatches an action, and the corresponding reducer updates the state.
Example Usage in a React Component:
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './features/counter/counterSlice';
export default function App() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(increment())}>+</button>
<span>Count: {count}</span>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
);
}- `useSelector` reads state from the store.
- `useDispatch` sends actions to the store.
4. Async Logic with createAsyncThunk
Handling async operations (like API calls) is easy with `createAsyncThunk`. It generates action types and handles loading, success, and error states.
Example:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Async thunk for fetching data
export const fetchData = createAsyncThunk(
'data/fetchData',
async (id, thunkAPI) => {
const response = await fetch(`/api/data/${id}`);
return response.json();
}
);
const dataSlice = createSlice({
name: 'data',
initialState: { value: null, status: 'idle', error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchData.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchData.fulfilled, (state, action) => {
state.status = 'succeeded';
state.value = action.payload;
})
.addCase(fetchData.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
},
});- `createAsyncThunk` handles the async flow.
- `extraReducers` lets you respond to actions generated by thunks.
Integrating Redux Toolkit into a React Project
Step 1: Install dependencies
npm install @reduxjs/toolkit react-reduxStep 2: Create your slice(s)
Organize each feature in its own folder with a slice file.
Step 3: Configure the store
Combine your slices in `store.js` using `configureStore`.
Step 4: Provide the store to React
Wrap your app in a `<Provider>` at the root:
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux';
import store from './redux/store';
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(
<Provider store={store}>
<App />
</Provider>
);Step 5: Use Redux state and actions in components
Use `useSelector` and `useDispatch` from `react-redux`.
Practical Tips
- Keep slices focused: One slice per feature (e.g., `counterSlice`, `userSlice`).
- Use Immer-powered reducers: Mutate state directly in reducers — Redux Toolkit handles immutability.
- Leverage `createAsyncThunk` for async logic: It simplifies API calls and async flows.
- Type your state (with TypeScript): For larger projects, TypeScript + Redux Toolkit is a great combo.
- Use Redux DevTools: `configureStore` enables them by default for easy debugging.
Conclusion
Redux Toolkit is the modern, recommended way to use Redux in React. It reduces boilerplate, encourages best practices, and makes state management approachable for all React developers. By organizing your logic with slices, using the powerful store setup, and handling async with thunks, you can build scalable, maintainable apps with ease.





