Photo by Lautaro Andreani on Unsplash
State Management in React with Redux Toolkit: A Step-by-Step Guide
Introduction
State management is crucial for creating dynamic, and scalable user interfaces. As applications become complex, so does the challenge of effectively managing state. Redux has long been the preferred solution for handling state in large React applications, but its complexity and boilerplate code can be challenging for developers.
Redux Toolkit provides a simpler, straightforward approach to state management. Whether you're an experienced developer or a beginner, mastering Redux Toolkit will equip you with the skills to build robust and maintainable applications.
In this step-by-step guide, we'll explore how Redux Toolkit addresses the common pain points of traditional Redux, and we'll build a React application using Redux toolkit to handle state management. By the end of this article, you'll have a solid understanding of how to leverage Redux Toolkit to manage state in your React application.
Understanding Redux
Redux is a state management library that helps you manage the state of your application. It provides a centralized store for your application's state, which allows you to maintain and update state in a structured manner.
Core concepts: Store, Actions, Reducers
To understand how Redux works, it is essential to understand its three core concepts: Store, Actions, and Reducers; Let's use a warehouse analogy to explain further.
Store: The store is like a warehouse where all your goods are kept. This warehouse serves as the single source of truth for your inventory. The store is a plain JavaScript object that acts as the single source of truth for your application's state.
Actions: They are like the delivery orders that come into the warehouse. These orders tell the workers what to do—whether it's adding new goods, removing some, or updating the quantity of existing items. Actions are payloads of information that send data from your application to the Redux store. Actions are plain javascript objects that must have a
type
property that describes the type of action being performed.Example of an action:
const incrementAction = { type: 'INCREMENT', payload: 1 };
Reducers: Reducers are like the workers who receive the delivery orders(actions) and make the necessary changes to the inventory. When an order(actions) arrives, the workers(reducers) look at the current state of the warehouse(store) and follow instructions in the delivery order to update the inventory.
Reducers are pure functions that determine how the state of the application changes in response to an action. They must be pure, meaning they should not mutate the existing state but return a new state object.
Example of a reducer:
function counterReducer(state = { count: 0 }, action) { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + action.payload }; default: return state; } }
Challenges with Traditional Redux
While Redux is powerful, it can be verbose and requires much boilerplate code. You need to create corresponding action types, action creators, and reducers for every action. Additionally, managing asynchronous logic(e.g. API calls) can become complex, requiring middleware like redux-thunk
or redux-saga
.
These challenges make Redux complex especially for smaller projects or for developers new to the library. This is where Redux Toolkit steps in, providing a more developer-friendly approach to working with Redux.
Introduction to Redux Toolkit
Redux toolkit is the official, recommended way to write Redux logic. It's a library that builds on top of Redux, providing a set of tools and best practices that make it easier to work with Redux. Redux toolkit allows developers to focus on building features rather than managing the intricacies of state management.
Setting Up Redux Toolkit in a React Project
Let's walk through the steps to set up Redux Toolkit, create your store, and define slices to manage your application's state.
Step 1: Installing Redux Toolkit and React-Redux
To do this, run the following command in your project's root directory:
npm install @reduxjs/toolkit react-redux
or, if you're using Yarn
yarn add @reduxjs/toolkit react-redux
Step 2: Setting Up the Redux Store
In your src
folder create a new folder called redux
and a file in that folder called store.js
Redux Toolkit provides a function called configureStore
that simplifies the process of creating a store.
Here's how you can set up a basic store:
// src/redux/store.js
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import usersReducer from './userSlice';
import productsReducer from './productSlice';
const rootReducer = combineReducers({
users: usersReducer,
products: productsReducer,
});
export const store = configureStore({
reducer: rootReducer,
});
combineReducers
helps you organize and manage multiple slices by merging them into one single root reducer, making it easier to maintain and scale your application's state management.
Step 3: Creating a Slice
Next, we create a slice
for our features. A slice is a collection of Redux reducer logic and actions for a specific feature of your application. createSlice
function allows you to define the initial state, reducers, and actions all in one place.
For demonstration purposes, we'll be using JSON placeholder as a dummy endpoint.
We start by importing createSlice
and createAsyncThunk
from Redux Toolkit and then define the initialState
for the slice.
// src/redux/usersSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
const initialState = {
users: null,
loading: false,
error: null,
};
Next, we create an asynchronous action called getAllUsers
using createAsyncthunk
. createAsyncthunk
generates actions for each stage of an asynchronous operation: pending, fulfilled, and rejected.
The getAllUsers
action fetches data from the JSON placeholder endpoint. If the request is successful, the response is returned. If it fails, we catch the error and pass it to the rejectWithValie
function, which will handle the error state in our slice.
// src/redux/usersSlice.js
export const getAllUsers = createAsyncThunk(
'users/allUsers',
async (_, thunkAPI) => {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/users');
return response;
} catch (error) {
return thunkAPI.rejectWithValue(error);
}
}
);
Then we create our users
slice using the creatSlice
function.
// src/redux/usersSlice.js
export const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(getAllUsers.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(getAllUsers.fulfilled, (state, action) => {
state.loading = false;
state.users = action.payload;
})
.addCase(getAllUsers.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
})
},
});
This slice doesn't need any reducers in the reducers
object because our actions are handled by the extraReducers
field. The extraReducers
allows us to respond to actions defined outside of the slice. We can add cases for different states of getAllUsers
in the extraReducers
using the builder
API.
Putting it all together we have:
// src/redux/usersSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
const initialState = {
users: null,
loading: false,
error: null,
};
export const getAllUsers = createAsyncThunk(
'users/allUsers',
async (_, thunkAPI) => {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/users');
return response.data;
} catch (error) {
return thunkAPI.rejectWithValue(error);
}
}
);
export const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(getAllUsers.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(getAllUsers.fulfilled, (state, action) => {
state.loading = false;
state.users = action.payload;
})
.addCase(getAllUsers.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
})
},
});
export default usersSlice.reducer;
Step 4: Integrating the Store with React
With the store setup and slices defined, the next step is to connect your React components to the Redux store. This is done using the Provider
component from react-redux
which makes the Redux store available to any nested components that need access to the state.
Wrap your root component with the Provider
and pass in the store:
// src/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')
);
Step 5: Accessing State and Dispatching Actions in Components
Now that your store is connected, any component within the application can access the Redux store and dispatch actions using React-Redux hooks like useSelector
and useDispatch
.
// src/pages/Users.js
import React, {useEffect} from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getAllUsers } from './usersSlice';
const Users = () => {
const {users, loading, error, } = useSelector(state => state.users);
const dispatch = useDispatch();
useEffect(()=>{
dispatch(getAllUsers())
}, [dispatch])
return (
<div>
<h1>All Users</h1>
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
{users?.map((user) => (
<div key={user.id}>
<p>{user.name}</p>
<p>{user.email}</p>
<p>{user.phone}</p>
<p>{user.website}</p>
</div>
))}
</div>
);
};
export default Users;
useSelector
allows you to extract data from the Redux store state.
useDispatch
returns a dispatch function from the Redux store which allows you to send actions to the store, in this case, getting all users.
Conclusion
In this article, we've explored the fundamentals of state management and why Redux Toolkit is a better option for managing state in React applications. We walked through the steps of setting up Redux Toolkit in a React project, creating slices, handling asynchronous actions with createAsyncThunk
, and connecting components to the Redux store using React-Redux hooks.
I hope this guide has provided you with a solid foundation for getting started with state management using Redux Toolkit.
For further reading, you can check out the official documentation for Redux Toolkit.