Using Redux Toolkit, and differences with action creators, state updates, and async actions

williamluck

william-luck

Posted on April 29, 2023

Using Redux Toolkit, and differences with action creators, state updates, and async actions

What are the advantages of using Redux Toolkit? I asked myself this question when I had spent a solid week trying to learn how Redux alone could be used in my applications, and then learned Redux Toolkit, which simplifies mostly everything I learned in the week previous.

The most helpful benefits of using the toolkit for me involve mutable state updates, generating action creators automatically, and handling asynchronous fetch requests. There are many more, especially in regard to simplifying syntax and removing much of the boilerplate code to even get Redux up and running, but these three benefits address the most challenging aspects I struggled with in using Redux.

Updating state

I will use examples from a recent application I developed, which involves CRUD actions for ingredients, menus, and products as part of my phase 5 project for Flatiron School.

When I want to add ingredients to the Redux state without the use of the toolkit, I cannot change the state directly, but I have to return the previous form of state with any new additions that override the previous data in my reducer:

function ingredientsReducer(state = initialState, action) {
switch(action.type) {
case ingredients/ingredientAdded: 
    return {
        state,
        entities: [state.entities, action.payload]
    }
default: 
    return state
}
Enter fullscreen mode Exit fullscreen mode

Notice the use of the spread operator, I am never mutating state directly, only returning a new form of state to override any changes.

With the toolkit, I am able to write code that appears to mutate state (but not actually doing so because of some behinds the scenes toolkit magic, using Immer library):

const ingredientsSlice = createSlice({
    name: 'ingredients',
    initialState: {
        entities: [],
        menu: '', 
        name: ''
    },
    reducers: {
        ingredientAdded(state, action) {
            state.entities.push(action.payload)
        }
    }
    }
)
Enter fullscreen mode Exit fullscreen mode

The state updates are found under the reducers key in the ingredients slice (using createSlice, which is also specific to the toolkit). Instead of worrying about bug-prone mutable state updates with the use of the spread operator, I can write code that directly appears to push the new ingredient to the state mutably. For emphasis, the toolkit is handling the work behind the scenes to not actually mutate state, but I can write simpler and more digestible code.

But what is happening above with the created slice and specifying the state and reducers?

Generating Action Creators Automatically

Another change in the toolkit is that I no longer have to write out my action creators, and the toolkit handles those processes for me.

Action creators, without the toolkit, are functions that return an action object for dispatching actions to the store. An alternative of the above above when writing an action creator explicitly would be:

function(ingredientAdded)(ingredient) {
return { type: ingredients/ingredientAdded, payload: ingredient }
Enter fullscreen mode Exit fullscreen mode

With the toolkit, the action creators are generated automatically under the reducers key in the created slice. All we need to do to use them in our application to export them from the 'actions' property of the ingredientsSlice object.

export const {ingredientAdded, anyOtherReducers } = ingredientsSlice.actions;
Enter fullscreen mode Exit fullscreen mode

Asynchronous Actions

When using createSlice, we create an object with keys including name, state (where we store data as normal along with any errors or loading state needed), and reducers (which generate the action creators automatically). Another key we can define is extraReducers.

The difference between the reducers key on the extraReducers is that the reducers key creates those action creator functions and responds to the actions within the reducer. The extraReducers do not create an action creator function, but responds to actions we have defined somewhere else.

Without the toolkit, we would stick with explicitly defining an action creators and make a fetch requests, and return the type and payload depending on the response from the fetch request itself. But since the fetch requests are asynchronous, and even though we can chain a series of .then()s to the request, any action creators will return the action even though the promise has not resolved:

export function fetchMenus() {
const menus = fetch(/menus).then(r => r.json());

return {
 type: menus/fetchMenus, 
 payload: menus
}
}
Enter fullscreen mode Exit fullscreen mode

Without the toolkit, we would have to import and incorporate redux-thunk middleware by default to avoid this issue, and include it in the index file when creating the store:

import thunkMiddleware from "redux-thunk";

const store = createStore(rootReducer, applyMiddleware(thunkMiddleware));
Enter fullscreen mode Exit fullscreen mode

Then in the action creator, we dispatch actions as such:


export function fetchMenus() {

return function (dispatch) {

dispatch({type: menus/menusLoading })


fetch(/menus)
    .then(r => r.json());
    .then(menus => {
dispatch({type: menus/menusLoaded, payload: menus})
})

}}
Enter fullscreen mode Exit fullscreen mode

Notice that we are not actually returning any actions, but instead a function that dispatches action from the returned function, one for loading status and one for loaded status.

With the redux toolkit, it becomes simpler. We first create a separate exported function outside of our slice, and call this function directly in react components whenever applicable (such as page load, navigation away from the page, on button click, etc).

For this example, instead of fetching ingredients, I am fetching menus which I use to load data whenever the application first starts. We use createAsyncThunk provided by the toolkit, and keep the fetch request in a separate function:

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
Enter fullscreen mode Exit fullscreen mode

fetch request

Then in the extraReducers key, I can handle the loading and loaded status, as well as any updates to state, as such:

extraReducers

There are still two different cases for fetching menus, loading and fulfilled. Each changes state, but only fulfilled will update the state to include the menus returned from the fetchMenus function. This removes the need for applying the middleware explicitly, worrying about the promise not resolving before updating state.

💖 💪 🙅 🚩
williamluck
william-luck

Posted on April 29, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related