© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2021
D. B. Duldulao, R. J. L. CabagnotPractical Enterprise Reacthttps://doi.org/10.1007/978-1-4842-6975-6_10

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.
<Route exact path={path + '/calendar'}
                  component={lazy(
                  () => import('./views/dashboard/calendar/CalendarView'),
                  )} />
Listing 10-2

Registering the CalendarView in routes.tsx

Updating the Dashboard Sidebar Nav

After registering the calendar in the routes folder, we will add a calendar icon to the dashboard sidebar navigation.

Go to the dashboard-sidebar-navigation to update it. First, add the calendar icon from React Feather. Again, we’ll rename it as CalendarIcon.
import { PieChart as PieChartIcon,
        ShoppingCart as ShoppingCartIcon,
        ChevronUp as ChevronUpIcon,
        ChevronDown as ChevronDownIcon,
        Calendar as CalendarIcon,
        List as ListIcon,
        FilePlus as FilePlusIcon,
        LogOut as LogOutIcon,
} from 'react-feather';
Listing 10-3

Importing the Calendar Component to the dashboard-sidebar-navigation

Now that we’ve added that in the DashboardSidebarNavigation component, let’s put another menu below Create Product, as shown in Listing 10-4.
<ListSubheader>Applications</ListSubheader>
              <Link className={classes.link} to={`${url}/calendar`}>
              <ListItem button>
                <ListItemIcon>
                  <CalendarIcon/>
                </ListItemIcon>
                <ListItemText primary={'Calendar'} />
              </ListItem>
              </Link>
Listing 10-4

Creating a Calendar Icon Menu in the dashboard-sidebar-navigation

Refresh the browser to see the Calendar menu as shown in Figure 10-1.
../images/506956_1_En_10_Chapter/506956_1_En_10_Fig1_HTML.jpg
Figure 10-1

Showing Calendar in the UI

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 */
const ConnectedApp = ({ Component }: Props) => (
  <Provider store={store}>
    <HelmetProvider>
      <Component />
    </HelmetProvider>
  </Provider>
);
const render = (Component: typeof App) => {
  ReactDOM.render(<ConnectedApp Component={Component} />, MOUNT_NODE);
};
if (module.hot) {
  // Hot reloadable translation json files and app
  // modules.hot.accept does not accept dynamic dependencies,
  // have to be constants at compile-time
  module.hot.accept(['./app', './locales/i18n'], () => {
    ReactDOM.unmountComponentAtNode(MOUNT_NODE);
    const App = require('./app').App;
    render(App);
  });
}
render(App);
// 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.
../images/506956_1_En_10_Chapter/506956_1_En_10_Fig2_HTML.jpg
Figure 10-2

Deleting the _tests_ folder inside store and types folder

Next, locate the utils folder and DELETE everything EXCEPT the bytes-to-size.ts file.
../images/506956_1_En_10_Chapter/506956_1_En_10_Fig3_HTML.jpg
Figure 10-3

Deleting the utils folder

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.

../images/506956_1_En_10_Chapter/506956_1_En_10_Figa_HTML.jpg
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 state here refers to the CalendarState */
  reducers: {
    setLoading(state, action: PayloadAction<boolean>) {
      state.loading = action.payload;
    },
    setError(state, action: PayloadAction<string>) {
      state.error = action.payload;
    },
    getEvents(state, action: PayloadAction<EventType[]>) {
      state.events = action.payload;
    },
  },
});
/* Asynchronous actions. Actions that require Axios (HTTP client)
 or any APIs of a library or function that returns a promise. */
export const getEvents = (): AppThunk => async dispatch => {
  dispatch(slice.actions.setLoading(true));
  dispatch(slice.actions.setError(''));
  try {
    const response = await axios.get<EventType[]>(EndPoints.events);
    dispatch(slice.actions.getEvents(response.data));
  } catch (error) {
    console.log(error.message);
    dispatch(slice.actions.setError(error.message));
  } finally {
    dispatch(slice.actions.setLoading(false));
  }
};
export default slice.reducer;
Listing 10-15

Adding the Namespace and createSlice

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 DevToolsNetworkResponse to see the Events data, as shown in Figure 10-4.
../images/506956_1_En_10_Chapter/506956_1_En_10_Fig4_HTML.jpg
Figure 10-4

Showing a screenshot of Events data at the Chrome DevTools

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.
../images/506956_1_En_10_Chapter/506956_1_En_10_Fig5_HTML.jpg
Figure 10-5

Demonstrating IntelliSense through RootState

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.
../images/506956_1_En_10_Chapter/506956_1_En_10_Fig6_HTML.jpg
Figure 10-6

Rendering the CalendarView in the UI

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.90.142.26