Introduction to Redux Toolkit
Redux Toolkit (RTK) is the official, opinionated, batteries-included toolset for efficient Redux development. It was created to address three common concerns about Redux:
- "Configuring a Redux store is too complicated"
- "I have to add a lot of packages to get Redux to do anything useful"
- "Redux requires too much boilerplate code"
Redux Toolkit packages all the Redux essentials in one place, providing utilities to simplify common Redux use cases.
// Installing Redux Toolkit and React-Redux
npm install @reduxjs/toolkit react-redux
// or
yarn add @reduxjs/toolkit react-redux
Real-World Analogy: Home Building Toolkit
Redux Core is like being given the raw materials to build a house—lumber, nails, concrete, and tools. You have complete flexibility, but you need to make many decisions and perform many steps to build even a basic structure.
Redux Toolkit is like being given a premium homebuilder's toolkit that includes:
- Pre-made wall frames (createSlice)
- Power tools instead of hand tools (Immer for immutability)
- Best practice templates and guides (built-in middleware)
- Construction team with experience (opinionated defaults)
You still control the final design, but much of the repetitive work and decision-making is already handled, allowing you to build faster with fewer mistakes.
Key Features of Redux Toolkit
1. configureStore()
Wraps the Redux createStore function with simplified configuration options and good defaults. It automatically sets up:
- A combined reducer from your slice reducers
- Redux DevTools Extension integration
- Redux Thunk middleware for async actions
- Development-mode middleware for catching common mistakes
// Before (Redux core)
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import { usersReducer, postsReducer, commentsReducer } from './reducers';
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer,
comments: commentsReducer
});
const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware));
const store = createStore(rootReducer, composedEnhancer);
// After (Redux Toolkit)
import { configureStore } from '@reduxjs/toolkit';
import { usersReducer, postsReducer, commentsReducer } from './reducers';
const store = configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
comments: commentsReducer
}
// middleware, devTools, preloadedState, and enhancers are all configurable
});
2. createAction()
A utility that simplifies creating Redux action creators. It generates an action creator function and automatically uses the function name as the action type.
// Before (Redux core)
const ADD_TODO = 'todos/addTodo';
const addTodo = (text) => ({
type: ADD_TODO,
payload: { text, id: nanoid() }
});
// After (Redux Toolkit)
import { createAction } from '@reduxjs/toolkit';
import { nanoid } from '@reduxjs/toolkit';
const addTodo = createAction('todos/addTodo', (text) => ({
payload: { text, id: nanoid() }
}));
// The action creator's type is available as a property
console.log(addTodo.type); // 'todos/addTodo'
// You can still call it like a regular action creator
const action = addTodo('Write code');
console.log(action);
// { type: 'todos/addTodo', payload: { text: 'Write code', id: '4AJvwMSWEHCchcWYga3dj' } }
3. createReducer()
Provides a way to create reducers that use a lookup table approach instead of switch statements. It also uses Immer internally, which allows you to write "mutating" code that is actually handled immutably.
// Before (Redux core)
const initialState = { value: 0 };
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'counter/increment':
return {
...state,
value: state.value + 1
};
case 'counter/decrement':
return {
...state,
value: state.value - 1
};
case 'counter/incrementByAmount':
return {
...state,
value: state.value + action.payload
};
default:
return state;
}
}
// After (Redux Toolkit)
import { createReducer } from '@reduxjs/toolkit';
const increment = createAction('counter/increment');
const decrement = createAction('counter/decrement');
const incrementByAmount = createAction('counter/incrementByAmount');
const initialState = { value: 0 };
const counterReducer = createReducer(initialState, (builder) => {
builder
.addCase(increment, (state) => {
// "Mutate" the state directly - Immer makes it immutable behind the scenes
state.value += 1;
})
.addCase(decrement, (state) => {
state.value -= 1;
})
.addCase(incrementByAmount, (state, action) => {
state.value += action.payload;
});
});
4. createSlice()
The crown jewel of Redux Toolkit—it combines createAction and createReducer to generate action creators, action types, and reducers all in one function call. This drastically reduces boilerplate code.
// Redux Toolkit's createSlice
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
}
}
});
// Export the generated action creators
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// Export the reducer
export default counterSlice.reducer;
5. Built-in Thunk Middleware and DevTools Support
Redux Toolkit includes Redux Thunk for handling async logic and automatically sets up the Redux DevTools Extension for debugging.
// Async action with createAsyncThunk
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await fetch(`https://api.example.com/users/${userId}`);
return await response.json();
}
);
const usersSlice = createSlice({
name: 'users',
initialState: { entities: {}, loading: 'idle' },
reducers: {
// Regular reducers here
},
extraReducers: (builder) => {
builder
.addCase(fetchUserById.pending, (state) => {
state.loading = 'loading';
})
.addCase(fetchUserById.fulfilled, (state, action) => {
state.entities[action.payload.id] = action.payload;
state.loading = 'idle';
})
.addCase(fetchUserById.rejected, (state, action) => {
state.loading = 'failed';
state.error = action.error.message;
});
}
});
6. RTK Query
Introduced in Redux Toolkit 1.6, RTK Query is a powerful data fetching and caching tool that eliminates the need to write data fetching logic by hand.
// RTK Query example
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
// Define the API service
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com/' }),
endpoints: (builder) => ({
// Define an endpoint to fetch posts
getPosts: builder.query({
query: () => 'posts'
}),
// Define an endpoint to fetch a single post
getPost: builder.query({
query: (id) => `posts/${id}`
}),
// Define an endpoint to create a post
createPost: builder.mutation({
query: (newPost) => ({
url: 'posts',
method: 'POST',
body: newPost
})
})
})
});
// Export generated hooks
export const { useGetPostsQuery, useGetPostQuery, useCreatePostMutation } = api;
Comparison: Traditional Redux vs. Redux Toolkit
| Task | Traditional Redux | Redux Toolkit |
|---|---|---|
| Setting up store | Manual: combineReducers, applyMiddleware, etc. | Automatic with configureStore |
| Creating actions | Action type constants + action creators | Action creators generated by createSlice |
| Immutable updates | Manual spreading: {...state, value: newValue} | "Mutable" code with Immer |
| Async logic | Install thunk + write async action creators | createAsyncThunk or RTK Query |
| DevTools setup | Manual configuration | Automatic |
| Entity management | Manual normalization | createEntityAdapter |
| Data fetching | Custom logic with thunks | RTK Query with hooks |
Benefits of Using Redux Toolkit
1. Less Boilerplate Code
Perhaps the most significant benefit of Redux Toolkit is the dramatic reduction in boilerplate code. By combining action creators, action types, and reducers into slices, you can write significantly less code.
2. Built-in Best Practices
Redux Toolkit enforces Redux best practices by default:
- Immutable state updates (via Immer)
- Action type namespacing
- DevTools integration
- Middleware setup
3. Simplified Immutability
With Immer integration, you can write reducers that appear to mutate state directly, but actually produce immutable updates. This makes your code more readable and less error-prone.
// Without Immer
const todosReducer = (state = [], action) => {
switch (action.type) {
case 'todos/todoAdded': {
return [
...state,
{
id: action.payload.id,
text: action.payload.text,
completed: false
}
];
}
case 'todos/todoToggled': {
return state.map(todo => {
if (todo.id !== action.payload) {
return todo;
}
return {
...todo,
completed: !todo.completed
};
});
}
default:
return state;
}
};
// With Immer (via Redux Toolkit)
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
todoAdded: (state, action) => {
// Can write "mutating" logic because Immer makes it immutable
state.push({
id: action.payload.id,
text: action.payload.text,
completed: false
});
},
todoToggled: (state, action) => {
// Find the todo and toggle its completed flag
const todo = state.find(todo => todo.id === action.payload);
todo.completed = !todo.completed;
}
}
});
4. Simplified Async Logic
Redux Toolkit provides createAsyncThunk for handling async operations, which automatically generates pending/fulfilled/rejected action types and dispatches them based on the promise's state.
// Traditional Redux with thunks
export const fetchTodos = () => async dispatch => {
dispatch({ type: 'todos/fetchTodos/pending' });
try {
const response = await fetch('/api/todos');
const data = await response.json();
dispatch({ type: 'todos/fetchTodos/fulfilled', payload: data });
} catch (error) {
dispatch({ type: 'todos/fetchTodos/rejected', error: error.toString() });
}
};
// Redux Toolkit with createAsyncThunk
export const fetchTodos = createAsyncThunk(
'todos/fetchTodos',
async () => {
const response = await fetch('/api/todos');
return response.json();
}
);
// Then in your slice:
const todosSlice = createSlice({
name: 'todos',
initialState: { entities: [], loading: 'idle' },
reducers: {
// Regular reducers here
},
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.loading = 'loading';
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.loading = 'idle';
state.entities = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.loading = 'failed';
state.error = action.error.message;
});
}
});
5. Improved Developer Experience
Redux Toolkit improves the developer experience by providing better error messages, type checking, and integration with development tools.
6. TypeScript Integration
Redux Toolkit is written in TypeScript and provides excellent type definitions, making it easier to use Redux in TypeScript projects.
// TypeScript with Redux Toolkit
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;
}
}
});
Real-World Example: E-commerce App
Let's consider how Redux Toolkit simplifies building an e-commerce application:
Features:
- Product catalog browsing
- Shopping cart management
- User authentication
- Order processing
Implementation with RTK:
- Store setup: One configureStore call with all reducers
- Product catalog: RTK Query for fetching and caching products
- Shopping cart: createSlice for cart management with Immer-powered reducers
- User auth: createAsyncThunk for login/registration flows
- Order processing: Combined createAsyncThunk and RTK Query for submission and tracking
This approach eliminates hundreds of lines of boilerplate while providing better performance and developer experience.
RTK Query: Data Fetching Revolution
RTK Query is a powerful addition to Redux Toolkit that provides a standardized way to fetch, cache, and update data in your Redux applications.
Key Features
- Declarative data fetching: Define endpoints once, use throughout your app
- Automatic caching: Results are cached based on query parameters
- Automatic refetching: Configurable polling and refetch-on-focus
- Deduplicated requests: Multiple components requesting the same data share one request
- Optimistic updates: Update UI immediately before server confirms changes
- Normalized cache: Efficiently store and update related data
- TypeScript support: Fully typed API for better development experience
Basic RTK Query Implementation
// src/features/api/apiSlice.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }),
tagTypes: ['Post', 'User'],
endpoints: (builder) => ({
// Query endpoints (GET requests)
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Post']
}),
getPost: builder.query({
query: (id) => `/posts/${id}`,
providesTags: (result, error, id) => [{ type: 'Post', id }]
}),
// Mutation endpoints (POST, PUT, DELETE requests)
addPost: builder.mutation({
query: (post) => ({
url: '/posts',
method: 'POST',
body: post
}),
invalidatesTags: ['Post']
}),
updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: patch
}),
invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }]
}),
deletePost: builder.mutation({
query: (id) => ({
url: `/posts/${id}`,
method: 'DELETE'
}),
invalidatesTags: (result, error, id) => [{ type: 'Post', id }]
})
})
});
// Export the auto-generated hooks
export const {
useGetPostsQuery,
useGetPostQuery,
useAddPostMutation,
useUpdatePostMutation,
useDeletePostMutation
} = apiSlice;
Using RTK Query Hooks in Components
// src/features/posts/PostsList.jsx
import React from 'react';
import { useGetPostsQuery } from '../api/apiSlice';
import PostItem from './PostItem';
export function PostsList() {
// The query hook automatically fetches data when the component mounts
const {
data: posts = [],
isLoading,
isSuccess,
isError,
error
} = useGetPostsQuery();
let content;
if (isLoading) {
content = <div>Loading...</div>;
} else if (isSuccess) {
content = posts.map(post => <PostItem key={post.id} post={post} />);
} else if (isError) {
content = <div>Error: {error.toString()}</div>;
}
return (
<section>
<h2>Posts</h2>
{content}
</section>
);
}
// src/features/posts/AddPostForm.jsx
import React, { useState } from 'react';
import { useAddPostMutation } from '../api/apiSlice';
export function AddPostForm() {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
// The mutation hook returns a trigger function and result object
const [addPost, { isLoading }] = useAddPostMutation();
const handleSubmit = async (e) => {
e.preventDefault();
try {
// Call the mutation trigger function
await addPost({ title, content }).unwrap();
// Reset form fields on success
setTitle('');
setContent('');
} catch (err) {
console.error('Failed to save the post: ', err);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="Post title"
required
/>
<textarea
value={content}
onChange={e => setContent(e.target.value)}
placeholder="Post content"
required
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Adding...' : 'Add Post'}
</button>
</form>
);
}
Why RTK Query Instead of Other Solutions?
RTK Query offers several advantages over other data fetching libraries:
| Feature | RTK Query | React Query | Apollo Client | Custom Redux/Thunk |
|---|---|---|---|---|
| Redux Integration | Native | Separate | Separate | Native |
| Caching | Automatic | Automatic | Automatic | Manual |
| Normalized Cache | Yes | Limited | Yes | Manual |
| DevTools | Redux DevTools | React Query DevTools | Apollo DevTools | Redux DevTools |
| Code Generation | Automatic hooks | No | Yes (GraphQL) | No |
| Optimistic Updates | Yes | Yes | Yes | Manual |
| Bundle Size Impact | Medium (if already using Redux) | Small | Large | Small |
If you're already using Redux, RTK Query provides seamless integration while offering capabilities that would otherwise require multiple packages or complex custom code.
Migration Strategies
If you're already using Redux in your application, you can migrate to Redux Toolkit incrementally:
Incremental Migration Steps
- Replace
createStorewithconfigureStore - Convert one reducer at a time to use
createSlice - Replace thunks with
createAsyncThunk - Gradually adopt RTK Query for data fetching
// Step 1: Replace createStore with configureStore
// Before
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from './reducers';
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunk))
);
// After
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';
const store = configureStore({
reducer: rootReducer
// thunk is included by default
// DevTools extension is enabled by default
});
// Step 2: Convert a reducer to createSlice (one at a time)
// Before
const initialState = { value: 0 };
export function counterReducer(state = initialState, action) {
switch (action.type) {
case 'counter/increment':
return { ...state, value: state.value + 1 };
case 'counter/decrement':
return { ...state, value: state.value - 1 };
default:
return state;
}
}
export const increment = () => ({ type: 'counter/increment' });
export const decrement = () => ({ type: 'counter/decrement' });
// After
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => {
state.value += 1;
},
decrement: state => {
state.value -= 1;
}
}
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
Potential Migration Challenges
- Complex reducer logic: May require refactoring to fit with createSlice pattern
- Custom middleware: Needs to be adapted to work with configureStore
- Selector pattern changes: May need to update component connection logic
- Third-party libraries: Some may need updates or replacements
Practice Activities
Activity 1: Redux Toolkit Store Setup
Create a new Redux application using Redux Toolkit:
- Set up a new React project (using Create React App or Vite)
- Install Redux Toolkit and React-Redux
- Create a Redux store using configureStore
- Set up the Provider component in your app
- Verify store setup using Redux DevTools
Activity 2: Creating Your First Slice
Implement a counter feature using createSlice:
- Create a counterSlice.js file
- Define an initial state with a value property
- Create increment, decrement, and incrementByAmount reducers
- Export the actions and reducer
- Add the slice reducer to your store
- Create a Counter component that uses the slice
- Test your implementation by dispatching actions
Activity 3: Exploring RTK Query
Set up RTK Query to fetch data from an API:
- Choose a simple API (like JSONPlaceholder)
- Create an API slice using createApi
- Define endpoints for fetching data
- Add the API slice to your store
- Create a component that uses the generated hooks
- Display loading states, data, and potential errors
- Test caching behavior by mounting/unmounting components
Summary
- Redux Toolkit is the official, opinionated toolkit for efficient Redux development
- Key features include configureStore, createReducer, createAction, and createSlice
- RTK simplifies common Redux tasks while enforcing best practices
- Immer integration allows "mutating" code in reducers while maintaining immutability
- createAsyncThunk simplifies async operations and handling loading states
- RTK Query provides powerful data fetching and caching capabilities
- Migration to Redux Toolkit can be done incrementally for existing Redux applications
- Redux Toolkit significantly reduces boilerplate while improving developer experience