Skip to content

Implement a global loading state with Redux Toolkit and Typescript

Why implement a global loading state

Sometimes when using Redux we want to have a global state that will trigger a loading component in our application. The problem comes when we have multiple slices, the loading state might be in a different slice than the one we are using to dispatch the action so, how do we trigger the loding state from a different slice? There are different aproaches we can use to solve this problem, I will show you each one and create a test project to show you how to implement them.

Solution 1 Add matchers to your global slice (using async thunk)

The first solution is add matchers to the extra reducers, a matcher allows you to match your incoming actions against a custom filter function and execute logic if the criteria is met. In this case we will add 3 matchers, one when the action is loading, that will set the loading state to true, one when it's fullfilled and one when it's rejected, this last ones will change the loading state to false. This way the loading state is true once the action starts and then becomes false when the action finished or finds an error. We only need to add :load to the name of the action to use this matchers.

// Slice where the loading state is

import { AnyAction, createSlice } from "@reduxjs/toolkit";

interface GlobalSliceInitialState {
    loading: boolean;
}

export const initialState: GlobalSliceInitialState = {
  loading: false,
};

const globalSlice = createSlice({
  name: "global",
  initialState,
  extraReducers: (builder) => {
    builder
      .addMatcher(
        (action: AnyAction) =>
          action.type.includes("/pending") && action.type.includes(":load"),
        (state) => {
          state.loading = true;
        },
      )
      .addMatcher(
        (action: AnyAction) =>
          action.type.includes("/fulfilled") && action.type.includes(":load"),
        (state) => {
          state.loading = false;
        },
      )
      .addMatcher(
        (action: AnyAction) =>
          action.type.includes("/rejected") && action.type.includes(":load"),
        (state) => {
          state.loading = false;
        },
      );
  },
  reducers: {},
});

export default globalSlice.reducer;
// Async thunk from other slice using loading matcher

import { createAsyncThunk } from "@reduxjs/toolkit";

export const getAllTeamsMatcher = createAsyncThunk(
  "teams/getTeamsMatcher:load",
  async (payload, { rejectWithValue }) => {
    const teamsResult = await fetch(
      `https://www.balldontlie.io/api/v1/teams`,
    );
    if (teamsResult.status !== 200) {
      return rejectWithValue(await teamsResult.json());
    }
    return teamsResult.json();
  },
);  

Solution 2 Dispatch action from async thunks

The second solution is dispatch an action that changes the loading state from the async thunk. The action can be in any slice we just need to import the setLoading action and dispatch it, like in the example below.

// Slice where the loading state is

import { AnyAction, createSlice } from "@reduxjs/toolkit";

interface GlobalSliceInitialState {
    loading: boolean;
}

export const initialState: GlobalSliceInitialState = {
  loading: false,
};

const globalSlice = createSlice({
  name: "global",
  initialState,
  extraReducers: (builder) => {
  },
  reducers: {
    setLoading: (state, action) => {
      state.loading = action.payload;
    },
  },
});

export const { setLoading } = globalSlice.actions;

export default globalSlice.reducer;
// Async thunk from other slice dispatching set loading action

import { createAsyncThunk } from "@reduxjs/toolkit";
import { setLoading } from "../global";

export const getAllTeamsDispatch = createAsyncThunk(
    "teams/getTeamsDispatch",
    async (payload, { dispatch, rejectWithValue }) => {
      dispatch(setLoading(true))
      const teamsResult = await fetch(
        `https://www.balldontlie.io/api/v1/teams`,
      );
      if (teamsResult.status !== 200) {
        dispatch(setLoading(false))
        return rejectWithValue(await teamsResult.json());
      }
      dispatch(setLoading(false))
      return teamsResult.json();
    },
);  

Solution 3 Dispatch action from other slices (Not using async thunk)

The third solution is creating an action without using async thunks and use the dispatch from the parameter to dispatch the setLoading action.

// Slice where the loading state is

import { AnyAction, createSlice } from "@reduxjs/toolkit";

interface GlobalSliceInitialState {
    loading: boolean;
}

export const initialState: GlobalSliceInitialState = {
  loading: false,
};

const globalSlice = createSlice({
  name: "global",
  initialState,
  extraReducers: (builder) => {
  },
  reducers: {
    setLoading: (state, action) => {
      state.loading = action.payload;
    },
  },
});

export const { setLoading } = globalSlice.actions;

export default globalSlice.reducer;
// Action from other slice dispatching set loading action

import { setLoading } from "../global";

export const getAllTeamsActionDispatch = () => (dispatch: any) => {
    return new Promise(async (resolve, reject) => {
        try {
            dispatch(setLoading(true))
            const teamsResult = await fetch(
              `https://www.balldontlie.io/api/v1/teams`,
            );
            if (teamsResult.status !== 200) {
              dispatch(setLoading(false))
              return reject("Error fetching team");
            }
            dispatch(setLoading(false))
          } catch (e) {
            dispatch(setLoading(false))
            reject("Error fetching team");
        }
    })
}
 

Create test project

For this test project we will use balldontlie api to load NBA teams and change the loading state from the global slice. First we need to create a React application. We can use "Create React App", copy and paste the following commands in your terminal.

Install Create React App

npm install -g create-react-app

Create typescript React app

npx create-react-app global-loading-state-redux-typescript --template typescript

Initialize react app

cd global-loading-state-redux-typescript
npm run start

If everything went right, you should be able to access the react application in this url http://localhost:3000/

Installing npm modules

This is a list of all the packages needed and the reason why we will install them:

"Redux Toolkit" (npm install @reduxjs/toolkit): Redux Toolkit 
standard way to write Redux logic.
"React Redux" (npm install react-redux): Allows a React application 
to use Redux Logic
"Redux" (npm install redux): Predictable state container for JavaScript apps.
using Node

Creating Redux store and slices

First create a new folder called store inside the src folder. Inside of this new folder create a new file called index.ts

// ./src/store/index.ts

import { configureStore } from "@reduxjs/toolkit";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import { combineReducers } from "redux";

// Import global slice
import globalReducer from "./global"
// import team slice
import teamReducer from "./team"

// Combine all reducer into one
const reducer = combineReducers({
  globalReducer,
  teamReducer
});

// Initialize store with all the combined reducers
const store = configureStore({
  reducer,
});

// Typescript types of the dispatch and states
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;

// useDispatch and useSelector hooks with types
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

// State selector for the team slice with types
export const teamSelector = (state: RootState) => state.teamReducer;
// State selector for the global slice with types
export const globalSelector = (state: RootState) => state.globalReducer;

export default store;

Now create a new folder called global inside of the store folder. This folder will contain all the global slice logic. Let's create a file called index.ts inside of the global folder.

// ./src/store/global/index.ts

import { AnyAction, createSlice } from "@reduxjs/toolkit";

// Global slice initial state fields
interface GlobalSliceInitialState {
    loading: boolean;
}

// Global slice initial state
export const initialState: GlobalSliceInitialState = {
  loading: false,
};

// Global slice
const globalSlice = createSlice({
  name: "global",
  initialState,
  extraReducers: (builder) => {
    builder
      .addMatcher(
        (action: AnyAction) =>
          action.type.includes("/pending") && action.type.includes(":load"),
        (state) => {
          state.loading = true;
        },
      )
      .addMatcher(
        (action: AnyAction) =>
          action.type.includes("/fulfilled") && action.type.includes(":load"),
        (state) => {
          state.loading = false;
        },
      )
      .addMatcher(
        (action: AnyAction) =>
          action.type.includes("/rejected") && action.type.includes(":load"),
        (state) => {
          state.loading = false;
        },
      );
  },
  reducers: {
    setLoading: (state, action) => {
      state.loading = action.payload;
    },
  },
});

export const { setLoading } = globalSlice.actions;

export default globalSlice.reducer;

Next create another folder called team inside of the store folder. This folder will contain all the team logic. Add a new file called index.ts

// ./src/store/team/index.ts

import { createSlice } from "@reduxjs/toolkit";
import { getAllTeamsDispatch, getAllTeamsMatcher } from "./actions";

// Team fields
interface Team {
    id: string;
    abbreviation: string;
    city: string;
    conference: string;
    division: string;
    full_name: string;
    name: string;
}

// Team slice initial state fields
interface TeamSliceInitialState {
    teams: Team[];
}

// Team slice initial state
export const initialState: TeamSliceInitialState = {
    teams: [],
};

// Team slice
const teamSlice = createSlice({
  name: "team",
  initialState,
  extraReducers: (builder) => {
    builder.addCase(
        getAllTeamsMatcher.fulfilled,
        (state, action) => {
          state.teams = action.payload.data;
        },
    );
    builder.addCase(
        getAllTeamsDispatch.fulfilled,
        (state, action) => {
          state.teams = action.payload.data;
        },
    )
  },
  reducers: {
    setTeams: (state, action) => {
        state.teams = action.payload.data;
      },
  },
});

export const { setTeams } = teamSlice.actions;

export default teamSlice.reducer;

Creating async thunks and action

Now we will create the async thunks to get information from an API and trigger the loading state. Add a new file called actions.ts inside of the main slice folder.

// ./src/store/team/actions.ts

import { createAsyncThunk } from "@reduxjs/toolkit";
import { setLoading } from "../global";
import { setTeams } from ".";

/**
 * @desc gets all NBA teams and sets the loading state to true using matchers
 */
export const getAllTeamsMatcher = createAsyncThunk(
  "teams/getTeamsMatcher:load",
  async (payload, { rejectWithValue }) => {
    const teamsResult = await fetch(
      `https://www.balldontlie.io/api/v1/teams`,
    );
    if (teamsResult.status !== 200) {
      return rejectWithValue(await teamsResult.json());
    }
    return teamsResult.json();
  },
);


/**
 * @desc gets all NBA teams and sets the loading state to true dispatching action
 */
export const getAllTeamsDispatch = createAsyncThunk(
    "teams/getTeamsDispatch",
    async (payload, { dispatch, rejectWithValue }) => {
      dispatch(setLoading(true))
      const teamsResult = await fetch(
        `https://www.balldontlie.io/api/v1/teams`,
      );
      if (teamsResult.status !== 200) {
        dispatch(setLoading(false))
        return rejectWithValue(await teamsResult.json());
      }
      dispatch(setLoading(false))
      return teamsResult.json();
    },
);

/**
 * @desc gets all NBA teams and sets the loading state to true dispatching action
 */
export const getAllTeamsActionDispatch = () => (dispatch: any) => {
    return new Promise(async (resolve, reject) => {
        try {
            dispatch(setLoading(true))
            const teamsResult = await fetch(
              `https://www.balldontlie.io/api/v1/teams`,
            );
            if (teamsResult.status !== 200) {
              dispatch(setLoading(false))
              return reject("Error fetching team");
            }
            const teamsDataResult = await teamsResult.json();
            dispatch(setLoading(false))
            dispatch(setTeams(teamsDataResult));
            return resolve(teamsDataResult);
          } catch (e) {
            dispatch(setLoading(false))
            reject("Error fetching team");
        }
    })
}

Adding redux provider

Now we will add the redux provider to our application, for this we need to edit the index.ts file located in the src directrory.

// src/index.ts

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import store from "./store"

import { Provider } from 'react-redux';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Changin App component to dispatch async thunks and action

Now we will change the App component to add some buttons to dispatch our action and see how the loading state changes. We will edit the App.tsx file inside of the src folder.

// src/App.tsx

import React from 'react';
import './App.css';
import { useAppSelector, globalSelector, useAppDispatch, teamSelector } from './store';
import { getAllTeamsActionDispatch, getAllTeamsDispatch, getAllTeamsMatcher } from './store/team/actions';

function App() {
  // Load state from slices
  const { loading } = useAppSelector(globalSelector);
  const { teams } = useAppSelector(teamSelector);

  // Use dispatch hook
  const dispatch = useAppDispatch();

  /**
   * @desc gets NBA teams
   */
  const handleGetTeamsMatcher = () => {
    // Dispatch async thunk
    dispatch(getAllTeamsMatcher())
  }

   /**
   * @desc gets NBA teams
   */
  const handleGetTeamsDispatch = () => {
    // Dispatch async thunk
    dispatch(getAllTeamsDispatch())
  }

   /**
   * @desc gets NBA teams
   */
  const handleGetTeamsActionDispatch = () => {
    // Dispatch action
    dispatch(getAllTeamsActionDispatch())
  }

  return (
    <div className="App">
      <header className="App-header">
        <div 
          style={{
            display: 'flex', 
            flexDirection: 'column',
            paddingBottom: '1rem'
          }}
        >
          <p>Current loading state: {loading.toString()}</p>

          <button 
            style={{
              marginBottom: '1rem',
              padding: '0.8rem',
              fontSize: '1rem',
              borderRadius: '5px',
              border: 'none',
              cursor: 'pointer'
            }} 
            onClick={() => handleGetTeamsMatcher()}
          >
            Fetch teams async thunk matchers changing state
          </button>
          <button
            style={{
              marginBottom: '1rem',
              padding: '0.8rem',
              fontSize: '1rem',
              borderRadius: '5px',
              border: 'none',
              cursor: 'pointer'
            }} 
            onClick={() => handleGetTeamsDispatch()}
          >
            Fetch teams async thunk dispatch changing state
          </button>
          <button
             style={{
              marginBottom: '1rem',
              padding: '0.8rem',
              fontSize: '1rem',
              borderRadius: '5px',
              border: 'none',
              cursor: 'pointer'
            }} 
            onClick={() => handleGetTeamsActionDispatch()}
          >
            Fetch teams action dispatch changing state
          </button>

          <p>Teams: </p>
          {
            teams.map((singleTeam) => {
              return <p key={singleTeam.id} style={{margin: '0.2rem'}}>{singleTeam.full_name}</p>
            })
          }
        </div>
     
      </header>
      
    </div>
  );
}

export default App;

GitHub repository

In this repository https://github.com/obravocedillo/GlobalLoadingStateReduxTypescript you can find the code.