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.
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();
},
);
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();
},
);
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");
}
})
}
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/
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
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;
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");
}
})
}
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();
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;
In this repository https://github.com/obravocedillo/GlobalLoadingStateReduxTypescript you can find the code.