State Management in React with Redux Toolkit: A Step-by-Step Guide

Hi, I’m Chineta Adinnu! I’m a frontend developer with a passion for creating dynamic and user-friendly web experiences. On this blog, I share my journey in frontend development, from learning new technologies to implementing them in real projects.
I dive into frameworks like React and Next.js, explore state management with Redux Toolkit, and offer practical tips based on my hands-on experience. My goal is to provide valuable insights and tutorials that can help others navigate the ever-evolving world of web development.
Join me as I document my learning process, share challenges and solutions, and offer guidance on the latest frontend technologies. If you’re interested in tech or looking for practical advice, you’re in the right place!
Feel free to connect if you have questions or want to discuss tech!
Check out some of my articles on Medium: https://medium.com/@chinetaadinnu."
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
typeproperty 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.




