Redux Toolkit
Modern Redux state management made simple
🔧 What is Redux Toolkit?
Redux Toolkit is the official, recommended way to write Redux logic. It simplifies Redux setup with less boilerplate code, built-in best practices, and powerful tools like createSlice and configureStore for efficient state management.
import { configureStore, createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => { state.value += 1; }
}
});
Result:
Redux Toolkit reduces boilerplate and makes Redux easier to use.
Core Redux Toolkit APIs
configureStore
Creates Redux store with defaults
const store = configureStore({
reducer: { counter: counterReducer }
});
createSlice
Generates actions and reducers
const slice = createSlice({
name: 'todos',
initialState: [],
reducers: { addTodo }
});
createAsyncThunk
Handles async logic easily
const fetchUser = createAsyncThunk(
'users/fetch',
async (id) => await api.get(id)
);
Hooks
Access state and dispatch
const count = useSelector(s => s.count);
const dispatch = useDispatch();
🔹 Setting Up Redux Toolkit
Start by installing Redux Toolkit and React-Redux, then create a store with configureStore. This setup includes Redux DevTools and middleware automatically.
# Install Redux Toolkit and React-Redux
npm install @reduxjs/toolkit react-redux
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
const store = configureStore({
reducer: {
counter: counterReducer
}
});
export default store;
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
Output:
Redux store is now available to all components in your app.
🔹 Creating a Slice
A slice contains reducer logic and actions for a single feature. createSlice automatically generates action creators and action types, eliminating boilerplate code.
// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: (state) => {
// Redux Toolkit uses Immer, so you can "mutate" state
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
reset: (state) => {
state.value = 0;
}
}
});
// Export actions
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;
// Export reducer
export default counterSlice.reducer;
Output:
Actions and reducer are automatically generated from the slice.
🔹 Using Redux in Components
Use useSelector to read state and useDispatch to dispatch actions. These hooks connect your React components to the Redux store seamlessly.
// Counter.jsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount, reset } from './counterSlice';
function Counter() {
// Select state from store
const count = useSelector((state) => state.counter.value);
// Get dispatch function
const dispatch = useDispatch();
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => dispatch(increment())}>
Increment
</button>
<button onClick={() => dispatch(decrement())}>
Decrement
</button>
<button onClick={() => dispatch(incrementByAmount(5))}>
Add 5
</button>
<button onClick={() => dispatch(reset())}>
Reset
</button>
</div>
);
}
export default Counter;
Output:
Counter displays current value and buttons update the Redux state.
🔹 Async Operations with createAsyncThunk
createAsyncThunk handles async logic like API calls. It automatically dispatches pending, fulfilled, and rejected actions based on the promise lifecycle.
// userSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Async thunk for fetching user
export const fetchUser = createAsyncThunk(
'user/fetchUser',
async (userId) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
);
const userSlice = createSlice({
name: 'user',
initialState: {
data: null,
loading: false,
error: null
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
}
});
export default userSlice.reducer;
// UserProfile.jsx
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchUser } from './userSlice';
function UserProfile({ userId }) {
const dispatch = useDispatch();
const { data, loading, error } = useSelector((state) => state.user);
useEffect(() => {
dispatch(fetchUser(userId));
}, [dispatch, userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!data) return null;
return (
<div>
<h2>{data.name}</h2>
<p>{data.email}</p>
</div>
);
}
Output:
Shows loading state, then displays user data or error message.
🔹 Multiple Slices Example
Real apps have multiple slices for different features. Combine them in configureStore to create a complete state management solution.
// todosSlice.js
import { createSlice } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: (state, action) => {
state.push({
id: Date.now(),
text: action.payload,
completed: false
});
},
toggleTodo: (state, action) => {
const todo = state.find(t => t.id === action.payload);
if (todo) todo.completed = !todo.completed;
},
deleteTodo: (state, action) => {
return state.filter(t => t.id !== action.payload);
}
}
});
export const { addTodo, toggleTodo, deleteTodo } = todosSlice.actions;
export default todosSlice.reducer;
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
import todosReducer from './todosSlice';
import userReducer from './userSlice';
const store = configureStore({
reducer: {
counter: counterReducer,
todos: todosReducer,
user: userReducer
}
});
export default store;
Output:
Store now manages counter, todos, and user state independently.
🔹 Redux Toolkit Best Practices
Follow these guidelines to write clean, maintainable Redux code with Redux Toolkit:
✅ Do:
- Use createSlice for all state logic
- Use createAsyncThunk for async operations
- Keep slices focused on single features
- Use TypeScript for better type safety
- Organize slices by feature, not by type
❌ Don't:
- Write action types and creators manually
- Use plain Redux without Redux Toolkit
- Put all state in one giant slice
- Mutate state outside of createSlice reducers
- Forget to handle loading and error states
// ✅ Good: Focused slice with clear actions
const userSlice = createSlice({
name: 'user',
initialState: { profile: null, loading: false },
reducers: {
setProfile: (state, action) => {
state.profile = action.payload;
}
}
});
// ❌ Bad: Manual action types (unnecessary with RTK)
const SET_PROFILE = 'user/setProfile';
const setProfile = (payload) => ({ type: SET_PROFILE, payload });