Redux Toolkit (RTK) is the official, recommended way to write Redux logic. It was introduced to simplify common Redux tasks, reduce boilerplate, and enforce best practices. It abstracts away much of the boilerplate associated with configuring Redux, including setting up the store, writing reducers, and handling asynchronous logic (via createAsyncThunk).
Why Use Redux Toolkit?
- Simplifies Redux setup: Less configuration and boilerplate.
- Immutability and immutability safety: Uses Immer.js under the hood, allowing for safe, immutable updates while writing “mutable” code.
- Handles side effects: Comes with utilities like
createAsyncThunkto handle async logic. - Provides best practices: Encourages slice-based state management.
Key Concepts in Redux Toolkit
configureStore(): Sets up the Redux store with good defaults (like combining reducers, adding middleware).createSlice(): Automatically generates action creators and action types corresponding to the reducers and state you define.createAsyncThunk(): Simplifies handling asynchronous logic (like API calls).createReducer(): Provides a flexible way to define reducers that respond to actions.
Basic Redux Toolkit Example
Step 1: Installing Redux Toolkit
npm install @reduxjs/toolkit react-redux
Step 2: Creating a Slice
A slice combines your reducer logic and actions for a specific part of your Redux state.
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 const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
Explanation:
createSlicegenerates the Redux slice, including the reducer and actions automatically.increment,decrement, andincrementByAmountare the action creators.counterSlice.reduceris the reducer function, which we’ll use to configure the store.
Step 3: Configuring the Store
Use configureStore to set up the store with slices.
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export default store;
Step 4: Using Redux State and Actions in a Component
You can now access the Redux state and dispatch actions in your React components using useSelector and useDispatch from react-redux.
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount } from './counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<h1>{count}</h1>
<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>
<button onClick={() => dispatch(incrementByAmount(10))}>Increment by 10</button>
</div>
);
}
export default Counter;
useSelectoris used to extract data from the Redux store.useDispatchis used to dispatch actions likeincrementanddecrement.
Step 5: Providing the Store to the React App
Wrap the root component of your app with the Provider component from react-redux to give components access to the Redux store.
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import Counter from './Counter';
ReactDOM.render(
<Provider store={store}>
<Counter />
</Provider>,
document.getElementById('root')
);
Handling Asynchronous Logic with createAsyncThunk
For handling asynchronous logic like API calls, Redux Toolkit provides createAsyncThunk, which automatically handles the lifecycle of the async action (e.g., loading, success, and failure states).
Example: Fetching Data with createAsyncThunk
Let’s create a simple app that fetches data from an API using createAsyncThunk.
Step 1: Define an Async Thunk
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Define the async thunk for fetching data
export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
return response.json();
});
const postsSlice = createSlice({
name: 'posts',
initialState: {
posts: [],
loading: 'idle', // 'idle' | 'pending' | 'succeeded' | 'failed'
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.loading = 'pending';
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.loading = 'succeeded';
state.posts = action.payload;
})
.addCase(fetchPosts.rejected, (state, action) => {
state.loading = 'failed';
state.error = action.error.message;
});
},
});
export default postsSlice.reducer;
createAsyncThunkgenerates three action types:pending,fulfilled, andrejected.- The
extraReducersfield is used to handle actions generated bycreateAsyncThunk(like loading, success, and error states).
Step 2: Configuring the Store
import { configureStore } from '@reduxjs/toolkit';
import postsReducer from './postsSlice';
const store = configureStore({
reducer: {
posts: postsReducer,
},
});
export default store;
Step 3: Dispatching the Thunk in a Component
You can now dispatch the fetchPosts async action and display the fetched data in your component.
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchPosts } from './postsSlice';
function PostsList() {
const dispatch = useDispatch();
const posts = useSelector((state) => state.posts.posts);
const loading = useSelector((state) => state.posts.loading);
const error = useSelector((state) => state.posts.error);
useEffect(() => {
dispatch(fetchPosts());
}, [dispatch]);
if (loading === 'pending') {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
export default PostsList;
Example Breakdown:
- Async Thunk:
fetchPostsis dispatched on component mount to trigger the API call. - Loading and Error Handling: The
loadinganderrorstate is managed by Redux Toolkit’sextraReducers. - Rendering the Fetched Data: Once the data is successfully fetched, it is displayed in the component.
Summary of Core Features
configureStore():
- Automatically sets up the store with default middleware (e.g., Redux DevTools, thunk middleware).
- Combines reducers and applies middleware.
createSlice():
- A more convenient way to define reducers and action creators in one step.
- Automatically generates actions based on the reducer functions.
createAsyncThunk():
- Simplifies the handling of asynchronous logic like API requests.
- Generates actions for the three lifecycle states of a promise (pending, fulfilled, rejected).
createReducer():
- A flexible reducer creator that allows for both object notation and switch-case handling.
- Middleware and DevTools:
configureStoreenables Redux DevTools and middleware automatically, which provides a great development experience out of the box.
Why Redux Toolkit is Better for Modern Redux Development
- Less Boilerplate: Writing reducers, actions, and setting up middleware is much simpler.
- Immutable State Handling: Uses Immer under the hood, so you can “mutate” state directly in reducers without actually mutating it.
- Built-in Async Support:
createAsyncThunkmakes it easier to manage async actions like API calls. - Better DevTools Integration: Redux Toolkit automatically sets up the Redux DevTools extension.
- Encourages Best Practices: By default, RTK encourages slice-based architecture, proper store setup, and separation of concerns.
In summary, Redux Toolkit is the preferred way to work with Redux due to its simplicity, reduced boilerplate, and out-of-the-box best practices. It drastically improves the developer experience by making state management in React more efficient and scalable.
