If you have any experience with Redux, you might be familiar with Redux Toolkit. Redux Toolkit aims to simplify some of the setup for the store and reducers, and simplifies immutable update logic. Although Redux requires a significant amount of boilerplate code, Redux Toolkit makes it easier to get up and running. If you haven't used Redux Toolkit before, I'd recommend giving it a shot.
Installing Redux Toolkit
To begin, install Redux Toolkit in your project directory.
npm install @reduxjs/toolkit
Configuring the Store
The store is an essential part of Redux. The store is where your application's state will be stored and managed. Now that we've installed Redux Toolkit, let's initialize the store. Create a file called store.js
in the client directory of your project.
Let's configure the store like so:
// store.js
import { configureStore } from '@reduxjs/toolkit';
import usersReducer from './usersSlice';
const store = configureStore({
reducer: {
users: usersReducer
},
});
export default store;
We are going to be creating and importing some reducers, and adding them to the reducer object in our store. Using configureStore
allows us to combine multiple reducers in a single store.
Creating the Users Slice
In Redux Toolkit, slices are small, self-contained pieces of your Redux store that include reducers, actions, and async thunks. Create a new file called usersSlice.js
and define your usersSlice
:
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
//Reducer
const usersSlice = createSlice({
name: "users",
initialState: {
status: "idle",
entities: [],
errors: []
},
reducers: {},
extraReducers: {},
)
export default usersSlice.reducer;
Let's break this down a little bit. First, we are importing createAsyncThunk
and createSlice
. createAsyncThunk
will allow us to create functions that can dispatch actions (thunks) throughout our app in order to update state. createSlice
will allow us to create our usersSlice, which is home to our initial state and reducers, thereby responsible for maintaining and updating our User state.
Creating Async Actions
Now that we have our usersSlice setup, let's create our first thunk. When the app loads or a user signs in, we might want to fetch all of the user objects from our database. To do that, we need to first define a thunk like so:
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
export const fetchUsers = createAsyncThunk("users/fetchUsers", () => {
return fetch("/users")
.then((r) =>r.json())
})
//Reducer
const usersSlice = createSlice({
name: "users",
initialState: {
status: "idle",
entities: [],
errors: []
},
reducers: {},
extraReducers: {},
)
export default usersSlice.reducer;
Here, we created an action called fetchUsers, which is an async thunk. This thunk makes a fetch GET request to our proxy of "/users", and returns a response.
Creating Extra Reducers
To make updates to state with the response from our thunk, we need to create some cases for extraReducers.
These cases look for the action type ("users/fetchUsers"), and whether that action is currently pending, fulfilled, or rejected. Depending on the case, we can update state accordingly. Let's update our usersSlice to look like this:
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
export const fetchUsers = createAsyncThunk("users/fetchUsers", () => {
return fetch("/users")
.then((r) =>r.json())
})
//Reducer
const usersSlice = createSlice({
name: "users",
initialState: {
status: "idle",
entities: [],
errors: []
},
reducers: {},
extraReducers: (builder) => {
builder
//fetchUsers
.addCase(fetchUsers.pending, (state) => {
state.status = 'pending';
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'fulfilled';
state.entities = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = 'rejected';
state.errors = action.error.message;
});
},
)
export default usersSlice.reducer;
Here we've added 3 cases to the builder object which defines our reducers. State updates happen asynchronously when we make fetches to the database, so when the fetchUsers
thunk is first dispatched, it enters a pending state, so we change our initial status to pending
. If the request is successful, fetchUsers.fulfilled
will run, thereby changing status to fulfilled
and our entities array to the action.payload
(an array of User objects returned from the database). If the request is unsuccessful, the rejected
case will run, changing status to rejected
and errors to action.error.message
, which we can render to the page.
Connecting Redux to React
To connect the Redux store to a React app, use the Provider component from react-redux. Wrap the app with the provider in the index.js file, and pass it the store as a prop:
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from "react-redux";
import store from "./store"
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
Using Redux Toolkit in Components
Now that we have set up Redux Toolkit and the usersSlice
, we can start using it in our React components.
Fetching Users
To fetch users in a component, import the fetchUsers
async thunk and use the useDispatch
hook to dispatch the action:
// UserList.js
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchUsers } from './usersSlice';
const UserList = () => {
const dispatch = useDispatch();
const users = useSelector((state) => state.users.entities);
const status = useSelector((state) => state.users.status);
const errors = useSelector((state) => state.users.errors);
useEffect(() => {
dispatch(fetchUsers());
}, [dispatch]);
if (status === 'pending') {
return <div>Loading...</div>;
}
if (errors) {
return <div>Error: {error}</div>;
}
return (
<div>
<h2>User List</h2>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
};
export default UserList;
Here we are importing useEffect
in order to dispatch our fetchUsers
thunk/action when the app loads. We add dispatch
to the dependencies array to prevent some error warnings.
We are also importing useDispath
and useSelector
. useDispatch
allows us to dispatch thunks from our slices. useSelector
allows us access to our slices' state, and it subscribes the component to that piece of state, so whenever it changes, the component will rerender with the state update.
As you can see, we are conditionally rendering on whether the fetch has completed or returned an error. Upon successful fulfillment, we fetch the user objects and map over them to render a list of user names to the page.
Conclusion
Redux Toolkit, with its createAsyncThunk utility, simplifies handling asynchronous updates in your Redux application. By following the steps outlined in this article, you can create a "users" slice that efficiently fetches user data from an API and updates the state based on the fetch status.
With Redux Toolkit, you can focus on building features and user interfaces without getting bogged down by repetitive async action handling code. It's a valuable tool for streamlining your Redux development and ensuring smooth user experiences when dealing with async data.
In this article, we've explored how to use Redux Toolkit to manage asynchronous updates in a Redux store, specifically focusing on a "users" slice that fetches user data from an API. By following these steps, you can simplify the process of handling async actions and keep your Redux codebase clean and efficient.
Top comments (0)