10. Setting Up Redux Toolkit and Dispatching an Asynchronous Action
Devlin Basilan Duldulao1 and Ruby Jane Leyva Cabagnot1
(1)
Oslo, Norway
In the previous chapter, we learned the concept of managing the state using Redux Toolkit. We discussed prop drilling in a React app and showed the pattern when writing Redux in React.
Now, as promised, in this chapter, we are here to get down and dirty:
Setting up Redux Toolkit
Dispatching an asynchronous action to the reducer
Rendering the state from the Store to our UI – specifically, a calendar view
Creating the Calendar View Component
On that note, we’ll now create our calendar view component.
Open the dashboard directory, and we’ll create two folders, calendar and CalendarView, and the index.tsx file:
dashboard ➤ calendar ➤ CalendarView ➤ index.tsx
Open the index.tsx file and just add for now an h1 tag <Calendar Works!>, as shown in Listing 10-1.
import React from 'react';
const Index = () => {
return (
<div>
<h1>Calendar Works!</h1>
</div>
);
};
export default Index;
Listing 10-1
Creating index.tsx of CalendarView
Our next drill is updating the routes as we need to register the Calendar component in our routes.tsx.
Updating the Routes
Go to routes.tsx, and register the CalendarView. We can put it after the ProductCreateView, as shown in Listing 10-2.
Creating a Calendar Icon Menu in the dashboard-sidebar-navigation
Refresh the browser to see the Calendar menu as shown in Figure 10-1.
Now that we’ve seen it is working, let’s build the model for our calendar. In the models folder, add a file and name it calendar-type.ts. We’ll create the shape or model type of the CalendarView, as shown in Listing 10-5.
export type EventType = {
id: string;
allDay: boolean;
color?: string;
description: string;
end: Date;
start: Date;
title: string;
};
//union type
export type ViewType =
| 'dayGridMonth'
| 'timeGridWeek'
| 'timeGridDay'
| 'listWeek';
Listing 10-5
Creating the Shape or Model Type of the CalendarView
Okay, it’s time for the reducers to go inside the Store. Remember that reducers in Redux are what we use to manage the state in our application.
Reducers
We’re going to do some refactoring first, but we’ll make sure we won’t lose any core functionality of Redux.
Open the reducers.tsx and replace it with the code as shown in Listing 10-6. The inserted comments are a brief explanation of each.
/* Combine all reducers in this file and export the combined reducers.
combineReducers - turns an object whose values are different reducer functions into a single reducer function. */
import { combineReducers } from '@reduxjs/toolkit';
/* injectedReducers - an easier way of registering a reducer */
const injectedReducers = {
//reducers here to be added one by one.
};
/* combineReducers requires an object.we're using the spread operator (...injectedReducers) to spread out all the Reducers */
const rootReducer = combineReducers({
...injectedReducers,
});
/* RooState is the type or shape of the combinedReducer easier way of getting all the types from this rootReduder instead of mapping it one by one. RootState - we can use the Selector to give us intelli-sense in building our components. */
export type RootState = ReturnType<typeof rootReducer>;
export const createReducer = () => rootReducer;
Listing 10-6
Refactoring the reducers.ts
Next, we’ll also need to update the Store and simplify it. There’s currently Saga implementation there, but we don’t need it. We’ll use a more uncomplicated side effect – the Thunk.
Open configureStore.ts and refactor with the following code, as shown in Listing 10-7.
/*Create the store with dynamic reducers */
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import { forceReducerReload } from 'redux-injectors';
import { createReducer } from './reducers';
export function configureAppStore() {
const store = configureStore({
/*reducer is required. middleware, devTools, and the rest are optional */
reducer: createReducer(),
middleware: [
...getDefaultMiddleware({
serializableCheck: false,
}),
],
devTools: process.env.NODE_ENV !== 'production',
});
/* Make reducers hot reloadable, see http://mxs.is/googmo istanbul ignore next */
if (module.hot) {
module.hot.accept('./reducers', () => {
forceReducerReload(store);
});
}
return store;
}
Listing 10-7
Refactoring the configureStore.ts
Let’s further inspect what is going on in Listing 10-8.
In the Store setup, we are using the configureStore and getDefaultMiddleware from Redux Toolkit.
If you hover the cursor over the getDefaultMiddleware, you’ll see this message: “It returns an array containing the default middleware installed by ConfigureStore(). Useful if you want to configure your store with a custom middleware array but still keep the default setting.”
forceReduceReload from redux-injectors is for our hot reloading.
createReducer from the rootReducer is the function to return the combinedReducers.
middleware is an array of plugins or middleware.
store: We’ll need to inject this in our components through a provider.
And after that, let’s head off to the
src ➤ index.tsx
In React, if you see a component that a name provider is suffixing, this means that it is something you have to wrap in your root component.
A Provider component gives access to the whole application. In Listing 10-8, we are wrapping the root component (index.tsx) inside the Provider component.
/*wrapping the root component inside a provider gives all the component an access
to the provider component or the whole application */
const ConnectedApp = ({ Component }: Props) => (
<Provider store={store}>
<HelmetProvider>
<Component />
</HelmetProvider>
</Provider>
);
Listing 10-8
Wrapping the Root Component (index.tsx) Inside a Provider Component
The provider is being derived from React-Redux. This has been set up for us by the boilerplate.
Note that the provider has a required props store, and we’re passing into that the store that we created inside the configureStore.ts. That’s why we imported the configureAppStore from store/configureStore.
This makes the store the single source of truth – available to all components in our application.
Next, we need to update the index.tsx of the root component as shown in Listing 10-9. Keep in mind that this index.tsx is the entry file for the application – only for the setup and boilerplate code.
import 'react-app-polyfill/ie11';
import 'react-app-polyfill/stable';
import 'react-quill/dist/quill.snow.css';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import * as serviceWorker from 'serviceWorker';
import 'sanitize.css/sanitize.css';
// Import root app
import { App } from 'app';
import { HelmetProvider } from 'react-helmet-async';
import { configureAppStore } from 'store/configureStore';
// Initialize languages
import './locales/i18n';
const store = configureAppStore();
const MOUNT_NODE = document.getElementById('root') as HTMLElement;
interface Props {
Component: typeof App;
}
/*wrapping the root component inside a provider gives all the component an access
to the provider component or the whole application */
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
Listing 10-9
Updating the index.tsx of the Root Component
After this, let’s just do a bit of cleanup.
Cleanup Time
Delete the folder _tests_ inside the store folder. We’ll also take out the types folder because we already have a RootState.
Next, locate the utils folder and DELETE everything EXCEPT the bytes-to-size.ts file.
Updating Axios
That’s done. We are off to axios now to update the endpoints as shown in Listing 10-10.
Open src ➤ api ➤ axios.ts
export default api;
export const EndPoints = {
sales: 'sales',
products: 'products',
events: 'event',
};
Listing 10-10
Updating the Endpoints in axios.ts
Then let’s add another set of fake data in the db.json. Add the following Events data after products. The events array contains seven event objects.
Copy the code in Listing 10-11 and add it to the db.json file.
"events": [
{
"id": "5e8882e440f6322fa399eeb8",
"allDay": false,
"color": "green",
"description": "Inform about new contract",
"end": "2021-01-01T12:00:27.87+00:20",
"start": "2021-01-01T12:00:27.87+00:20",
"title": "Call Samantha"
},
{
"id": "5e8882eb5f8ec686220ff131",
"allDay": false,
"color": null,
"description": "Discuss about new partnership",
"end": "2021-01-01T12:00:27.87+00:20",
"start": "2021-01-01T12:00:27.87+00:20",
"title": "Meet with IBM"
},
{
"id": "5e8882f1f0c9216396e05a9b",
"allDay": false,
"color": null,
"description": "Prepare docs",
"end": "2021-01-01T12:00:27.87+00:20",
"start": "2021-01-01T12:00:27.87+00:20",
"title": "SCRUM Planning"
},
{
"id": "5e8882f6daf81eccfa40dee2",
"allDay": true,
"color": null,
"description": "Meet with team to discuss",
"end": "2020-12-12T12:30:00-05:00",
"start": "2020-11-11T12:00:27.87+00:20",
"title": "Begin SEM"
},
{
"id": "5e8882fcd525e076b3c1542c",
"allDay": false,
"color": "green",
"description": "Sorry, John!",
"end": "2021-01-01T12:00:27.87+00:20",
"start": "2021-01-01T12:00:27.87+00:20",
"title": "Fire John"
},
{
"id": "5e888302e62149e4b49aa609",
"allDay": false,
"color": null,
"description": "Discuss about the new project",
"end": "2021-01-01T12:00:27.87+00:20",
"start": "2021-01-01T12:00:27.87+00:20",
"title": "Call Alex"
},
{
"id": "5e88830672d089c53c46ece3",
"allDay": false,
"color": "green",
"description": "Get a new quote for the payment processor",
"end": "2021-01-01T12:00:27.87+00:20",
"start": "2021-01-01T12:00:27.87+00:20",
"title": "Visit Samantha"
}
]
Listing 10-11
Adding the Events Object in db.json
Implementing Redux Toolkit
Okay, now let’s do the fun part of implementing our Redux Toolkit.
We will be using two kinds of implementations in this application so you’ll understand how both work and it would be easier for you to onboard to an existing React–Redux Toolkit project.
The implementations are pretty much the same in many different projects you’ll soon encounter; sometimes, it’s just a matter of folder structuring and the number of files created.
Here we’re going to write all the actions and reducers in one file, and we will name it calendarSlice.ts.
Inside the src directory, create a new folder and name it features; this is where we will implement our Redux.
Inside the features, create a new folder and name it calendar. Inside the calendar, create a new file called calendarSlice.ts.
Redux Toolkit recommends adding the suffix Slice to your namespace.
Open the calendarSlice file, and let’s add some named imports (Listing 10-12).
/*PayloadAction is for typings */
import {
createSlice,
ThunkAction,
Action,
PayloadAction,
} from '@reduxjs/toolkit';
import { RootState } from 'store/reducers';
import { EventType } from 'models/calendar-type';
import axios, { EndPoints } from 'api/axios';
Listing 10-12
Adding the Named Import Components in calendarSlice
Next, let’s do the typings in calendarSlice as shown in Listing 10-13.
/*typings for the Thunk actions to give us intlelli-sense */
export type AppThunk = ThunkAction<void, RootState, null, Action<string>>;
/*Shape or types of our CalendarState */
interface CalendarState {
events: EventType[];
isModalOpen: boolean;
selectedEventId?: string; //nullable
selectedRange?: { //nullable
start: number;
end: number;
};
loading: boolean; //useful for showing spinner or loading screen
error: string;
}
Listing 10-13
Creating the Typings/Shapes in calendarSlice
And still in our calendarSlice file, we will initialize some values in our initialState, as shown in Listing 10-14.
/*initialState is type-safe, and it must be of a calendar state type.
It also means that you can't add any other types here that are not part of the calendar state we’ve already defined. */
const initialState: CalendarState = {
events: [],
isModalOpen: false,
selectedEventId: null,
selectedRange: null,
loading: false,
error: '',
};
Listing 10-14
Adding the Default Values of the initialState
And then, we move on to the creation of the namespace and the createSlice, as shown in Listing 10-15. We are adding the namespace and createSlice to the calendarSlice.
const calendarNamespace = 'calendar';
/*Single-File implementation of Redux-Toolkit*/
const slice = createSlice({
/*namespace for separating related states. Namespaces are like modules*/
name: calendarNamespace,
/*initialState is the default value of this namespace/module and it is required.*/
initialState, // same as initialState: initialState
/*reducers -- for non asynchronous actions. It does not require Axios.*/
The createSlice is a big object that requires us to put something in the name, the initialState, and the reducers.
The reducers here are an object of non-asynchronous actions (also known as synchronous actions) that do not require axios or are not promise-based.
Non-asynchronous Actions/Synchronous Actions
Let’s inspect what we have written in the non-async actions or synchronous actions inside our calendarSlice:
setLoading in reducers: There are two parameters (state and action), but you’re only required to pass the PayloadAction, a boolean.
setError in reducers: The same thing with the first parameter state; no need to pass anything because Thunk will take care of it under the hood. We just need to pass something or update the PayloadAction, which is a string.
getEvents in reducers: The PayloadAction is an array of EventType.
Asynchronous Actions
And here are our asynchronous actions:
getEvents: A function that returns AppThunk and a dispatch function.
dispatch(slice.actions.setLoading(true)): Updating the loading from default false to true.
dispatch(slice.actions.setError(' ')): We’re passing just an empty string, so basically, we’re resetting the error here back to empty every time we have a successful request.
Inside the try-catch block, we’re using an axios.get, and it’s returning an array of EventType from the Endpoints.events.
The response.data that we get will be dispatched to the Store so the state can be updated.
After creating the calendarSlice, we’ll now update the root reducers.
Updating the Root Reducer
Open the reducers.ts file again, and update the injectedReducers.
First, we need to import the calendarReducer from features/calendar/calendarSlice, as shown in Listing 10-16.
import { combineReducers } from '@reduxjs/toolkit';
import calendarReducer from 'features/calendar/calendarSlice'
Listing 10-16
Adding the Named Component in reducers.ts
And then, in the same file, inject our first reducer, as shown in Listing 10-17.
const injectedReducers = {
calendar: calendarReducer,
};
Listing 10-17
Injecting the calendarReducer in injectedReducers
We can now use this namespace calendar to get the needed state from this calendar. But we will do that in our components later on.
Now, we’re ready to write our selectors and dispatchers in the UI component of our calendar view or page.
Updating the CalendarView
But first, let’s test the dispatch by going to the calendar view component. Open the index.tsx of the CalendarView.
First, we’ll update the index.tsx of CalendarView, as shown in Listing 10-18.
import React, { useEffect } from 'react';
import { getEvents } from 'features/calendar/calendarSlice';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from 'store/reducers';
const CalendarView = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getEvents());
}, []);
Listing 10-18
Updating index.tsx of CalendarView
For now, we will check in the console the getEvents and useDispatch to see if we are successfully getting the data.
Make sure your server is running http://localhost:5000/events and click the refresh button in the browser http://localhost:3000/dashboard/calendar.
Open the Chrome DevTools ➤ Network ➤ Response to see the Events data, as shown in Figure 10-4.
Our proof of concept that our Redux is working! The state is in the browser, and we can use it. Let’s go back to our CalendarView component, and we’ll add the useSelector.
useSelector needs a function with a signature emitting and returning the RootState, and now we can access the reducer. For now, we can only access or get the calendar because this is what we’ve added so far, as shown in Figure 10-5.
We get IntelliSense through the use of RootState. If you’re using JavaScript instead of TypeScript, you’d have to guess or search for your reducer file(s). Imagine if you have an extensive application with dozens or even hundreds of files. Searching for it can quickly become tiresome.
This intelligent feature is one of the things where TypeScript shines. You can just type dot (.), and then it will show all the available reducers you can use.
Okay, let’s do some mapping now at our CalendarView.
return (
<div>
<h1>Calendar Works!</h1>
{loading && <h2>Loading... </h2>}
{error && <h2>Something happened </h2>}
<ul>
/*conditional nullable chain */
{events?.map(e => (
<li key={e.id}>{e.title} </li>
))}
</ul>
</div>
);
};
export default CalendarView;
Listing 10-19
Mapping the CalendarView in the UI
Okay, let’s inspect what we are doing in Listing 10-19.
loading &&: If the condition is true, the element right after && gets run; otherwise, if the state is false, ignore it. The same logic applies to the error &&.
Refresh the browser to check if you can see the loading before the data is rendered.
Summary
In this chapter, I hope you’ve gained a better understanding of the Redux implementation flow in a React app, including how to dispatch an async action to the reducer and render the state from the Store to the UI.
We also used the state management library Redux Toolkit and implemented its helper function called createSlice. We also expanded our styling components to include the calendar view component from Material-UI.
In the next chapter, we will continue with our Redux lessons to create, delete, and update events using Redux.