Introduction
Managing state in complex React applications can quickly become challenging. Prop drilling, inconsistent updates, and scattered logic often lead to bugs and maintenance headaches. Redux Toolkit—the official, opinionated, batteries-included toolset for Redux—simplifies state management by providing intuitive APIs, sensible defaults, and built-in best practices. In this post, we’ll introduce you to Redux Toolkit’s core concepts, walk through setting up a store and slices, demonstrate how to connect React components with hooks, and share advanced patterns for scaling your application. Whether you’re building a small widget or a large enterprise app, you’ll learn how Redux Toolkit streamlines state logic, reduces boilerplate, and keeps your code predictable and maintainable.

Why Redux Toolkit?
The Pain Points of Traditional Redux
- Boilerplate Overload: Action types, action creators, reducers, and switch statements—manually writing these for every feature can feel tedious.
- Immutable Updates: Manually copying nested objects to uphold immutability is error-prone.
- Configuration Complexity: Setting up the store, middleware (Thunk, Saga), and DevTools requires many lines of code.
How Redux Toolkit Helps
createSlice
API: Automatically generates action types and creators based on reducer functions.- Immer Integration: Write “mutating” logic in reducers; Immer produces immutably updated state under the hood.
configureStore
: Pre-configured store with Redux DevTools, Thunk middleware, and sensible defaults.- RTK Query (Optional): Built-in data fetching and caching layer to reduce boilerplate in async logic.
Getting Started
Installation
Install Redux Toolkit and React-Redux with:
bashCopyEditnpm install @reduxjs/toolkit react-redux
Folder Structure
A scalable pattern might look like:
bashCopyEditsrc/
├── app/
│ └── store.js
├── features/
│ ├── todos/
│ │ ├── todosSlice.js
│ │ └── TodosComponent.jsx
│ └── users/
│ ├── usersSlice.js
│ └── UsersComponent.jsx
└── index.js
Creating a Slice
Defining State Logic with createSlice
In todosSlice.js
, you can define your slice like this:
jsCopyEditimport { createSlice } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: {
items: [],
status: 'idle',
error: null
},
reducers: {
addTodo: (state, action) => {
state.items.push({ id: Date.now(), text: action.payload, completed: false });
},
toggleTodo: (state, action) => {
const todo = state.items.find(t => t.id === action.payload);
if (todo) todo.completed = !todo.completed;
}
}
});
export const { addTodo, toggleTodo } = todosSlice.actions;
export default todosSlice.reducer;
name
defines the slice’s namespace for action types (e.g.,"todos/addTodo"
).initialState
holds your slice’s default data.reducers
receive “mutating” logic; Immer ensures immutability.
Configuring the Store
In store.js
, you wire up your slices:

jsCopyEditimport { configureStore } from '@reduxjs/toolkit';
import todosReducer from '../features/todos/todosSlice';
import usersReducer from '../features/users/usersSlice';
export const store = configureStore({
reducer: {
todos: todosReducer,
users: usersReducer
}
});
configureStore
automatically sets up Redux Thunk and DevTools.
Providing the Store
Wrap your root component with the Redux Provider in index.js
:
jsxCopyEditimport React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
Accessing State and Dispatching Actions
useSelector
and useDispatch
Hooks
Replace connect
with hooks in functional components. For example, in TodosComponent.jsx
:
jsxCopyEditimport React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, toggleTodo } from './todosSlice';
export default function TodosComponent() {
const [text, setText] = useState('');
const todos = useSelector(state => state.todos.items);
const dispatch = useDispatch();
const handleAdd = () => {
if (text.trim()) {
dispatch(addTodo(text));
setText('');
}
};
return (
<div>
<h2>My Todos</h2>
<input value={text} onChange={e => setText(e.target.value)} placeholder="Add new todo" />
<button onClick={handleAdd}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id} onClick={() => dispatch(toggleTodo(todo.id))} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</li>
))}
</ul>
</div>
);
}
Handling Asynchronous Logic

Creating Thunks with createAsyncThunk
In usersSlice.js
, define an async thunk:
jsCopyEditimport { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
const response = await fetch('/api/users');
return response.json();
});
const usersSlice = createSlice({
name: 'users',
initialState: { list: [], status: 'idle', error: null },
reducers: {},
extraReducers: builder => {
builder
.addCase(fetchUsers.pending, state => { state.status = 'loading'; })
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'succeeded';
state.list = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
}
});
export default usersSlice.reducer;
Then in UsersComponent.jsx
:
jsxCopyEditimport React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchUsers } from './usersSlice';
export default function UsersComponent() {
const { list, status, error } = useSelector(state => state.users);
const dispatch = useDispatch();
useEffect(() => {
if (status === 'idle') dispatch(fetchUsers());
}, [status, dispatch]);
if (status === 'loading') return <p>Loading...</p>;
if (status === 'failed') return <p>Error: {error}</p>;
return (
<div>
<h2>Users</h2>
<ul>
{list.map(u => (
<li key={u.id}>{u.name} ({u.email})</li>
))}
</ul>
</div>
);
}
Advanced Patterns & Best Practices

RTK Query for Data Fetching
- Define endpoints to fetch, cache, and invalidate data with minimal boilerplate.
- Use auto-generated hooks like
useGetPostsQuery()
.
Slice Organization
- Feature Folders: Co-locate slice, component, and tests.
- Selectors: Extract reusable selectors to avoid duplication.
Performance Considerations
- Memoized Selectors: Use
createSelector
from Reselect. - Batching Updates: React-Redux batches dispatches in event handlers.
Conclusion
Redux Toolkit transforms complex state management into a streamlined, maintainable experience. By leveraging createSlice
, configureStore
, and createAsyncThunk
, you eliminate boilerplate, write safe “mutating” reducers, and integrate async logic cleanly. Advanced features like RTK Query, custom middleware, and memoized selectors help you scale confidently. Whether you’re building a simple counter or a data-rich dashboard, Redux Toolkit provides the structure and best practices you need to keep your React application predictable, performant, and easy to maintain.