DEV Community

Cody Barker
Cody Barker

Posted on

Simplify Async State Management with Redux Toolkit

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;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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)