Redux Toolkit
The official, modern way to write Redux logic with less boilerplate
Redux Toolkit (RTK)
Redux Toolkit is the official, opinionated, batteries-included toolset for efficient Redux development. It simplifies common Redux patterns and eliminates most of the boilerplate code. If you're using Redux today, you should be using Redux Toolkit.
What RTK Provides
- configureStore — Simplified store setup with good defaults
- createSlice — Generates action creators and reducers
- createAsyncThunk — Handles async logic and loading states
- createEntityAdapter — Normalized state management
- RTK Query — Built-in data fetching and caching
Installation
# New project
npx create-react-app my-app --template redux
# Existing project
npm install @reduxjs/toolkit react-redux
createSlice - The Core of RTK
import { createSlice } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: {
items: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
},
reducers: {
// RTK uses Immer - you can "mutate" state directly!
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;
}
},
deleteTodo: (state, action) => {
state.items = state.items.filter(t => t.id !== action.payload);
},
// Prepare callback for complex payloads
addTodoWithId: {
reducer: (state, action) => {
state.items.push(action.payload);
},
prepare: (text) => ({
payload: {
id: crypto.randomUUID(),
text,
completed: false,
createdAt: new Date().toISOString(),
},
}),
},
},
});
// Export auto-generated action creators
export const { addTodo, toggleTodo, deleteTodo, addTodoWithId } = todosSlice.actions;
// Export reducer
export default todosSlice.reducer;
configureStore
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './features/todos/todosSlice';
import userReducer from './features/user/userSlice';
export const store = configureStore({
reducer: {
todos: todosReducer,
user: userReducer,
},
// DevTools enabled by default in development
// Middleware includes redux-thunk by default
});
// TypeScript: Infer types from the store
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Async Logic with createAsyncThunk
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Create async thunk for fetching users
export const fetchUsers = createAsyncThunk(
'users/fetchUsers',
async (_, { rejectWithValue }) => {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Failed to fetch');
}
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// Thunk with arguments
export const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId, { rejectWithValue }) => {
try {
const response = await fetch(`/api/users/${userId}`);
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const usersSlice = createSlice({
name: 'users',
initialState: {
items: [],
status: 'idle',
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder
// Handle fetchUsers lifecycle
.addCase(fetchUsers.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload;
});
},
});
export default usersSlice.reducer;
Using in React Components
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, toggleTodo, deleteTodo } from './todosSlice';
import { fetchUsers } from './usersSlice';
function TodoList() {
const dispatch = useDispatch();
const todos = useSelector(state => state.todos.items);
const [text, setText] = useState('');
const handleAdd = () => {
if (text.trim()) {
dispatch(addTodo(text));
setText('');
}
};
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
/>
<button onClick={handleAdd}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
onClick={() => dispatch(toggleTodo(todo.id))}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</span>
<button onClick={() => dispatch(deleteTodo(todo.id))}>×</button>
</li>
))}
</ul>
</div>
);
}
function UsersList() {
const dispatch = useDispatch();
const { items: users, status, error } = useSelector(state => state.users);
useEffect(() => {
if (status === 'idle') {
dispatch(fetchUsers());
}
}, [status, dispatch]);
if (status === 'loading') return <div>Loading...</div>;
if (status === 'failed') return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Typed Hooks for TypeScript
// hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// Typed versions of useDispatch and useSelector
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// Usage in components
function MyComponent() {
const dispatch = useAppDispatch();
const todos = useAppSelector(state => state.todos.items);
// Full type safety!
}
Redux vs Redux Toolkit
| Feature | Classic Redux | Redux Toolkit |
|---|---|---|
| Action types | Manual constants | Auto-generated |
| Action creators | Manual functions | Auto-generated |
| Immutability | Spread operators | Immer (write mutations) |
| Store setup | Manual middleware | Good defaults built-in |
| Async logic | redux-thunk/saga | createAsyncThunk |
💡 Best Practices
- • Always use Redux Toolkit for new projects
- • Organize code by feature (feature folders)
- • Use createAsyncThunk for API calls
- • Take advantage of Immer's "mutable" syntax
- • Consider RTK Query for data fetching
- • Use typed hooks in TypeScript projects