Loader with React & Redux in TypeScript
Is Redux needed although now React has its own store API createContext and useContext? I don’t know. Nevertheless, I dare to write on Redux how to implement a loader UI with it in React project, simply because I‘ve done so in the real project recently and hopes that there is someone who wants to do the same thing out there, looking for the quick implementation, without the need of extra library.
Besides, I don‘t know exactly the tool came out but now Redux has Redux Toolkit, which they recommend developers to use. Some early adapters of Redux might be unfamiliar to this tool. This article can give those people an overview.
The term “loader” is ambiguous but here it looks something like below:
It is a UI component that is displayed on top of the page while you send a request to the remote server and wait for the response.
Not for beginners
This article is not about what Redux is. There are bunch of blog posts out there on the issue. This article is about how to implement the loader with Redux. If you are new to Redux looking for tutorials, you might be at a wrong place—though I would not deny that with no explanatory note the shortest path to know about Redux is to write hard-code.
Source code
The source code can be found at GitHub.
Getting started
Let’s create a project with create-react-app in a TypeScript template.
npx create-react-app react-redux-loader --template typescript
# or
yarn create create-react-app react-redux-loader --template typescript
Move to the new directory and install Redux packages:
npm install @reduxjs/toolkit react-redux
# or
yarn add @reduxjs/toolkit react-redux
Create new directories and files:
cd react-redux-loader
mkdir src/app src/features src/features/loader
touch src/app/store.ts src/app/hooks.ts
touch src/features/loader/loaderSlice.ts src/features/loader/Loader.ts src/features/loader/loader.css
I could use redux or redux-typescript template when run create-react-app. The reason I chose not to use them is that some readers are new to Redux. Creating and writing a store, a provider, and reducers can give them a clear sense of what the library is.
Redux
Creating slice
I have no idea where the term “slice” comes from—maybe portion of store. Anyway, Redux Toolkit has a function to create a slice createSlice. The slice is a kind of wrapper that contains reducers passed to the store and actions passed to dispatches.
The slice file would look something like this:
src/features/loaderSlice.ts
import { createSlice } from "@reduxjs/toolkit";
import type { RootState } from "../../app/store";
export interface loaderState {
status: 0 | 1;
};
export const loaderSlice = createSlice({
name: "loader",
initialState: {
status: 0
} as loaderState,
reducers: {
turnOn(state) {
state.status = 1;
},
turnOff(state) {
state.status = 0;
},
},
});
export const { turnOn, turnOff } = loaderSlice.actions
export function selectLoadingbarStatus(state: RootState): 1 | 0 {
return state.loader.status;
}
export default loaderSlice.reducer
Just ignore about the RootState at the moment.
As using TypeScript, we first define type of loader’s state, which has one property called "status", the value of which can be either 0 or 1.
We have two reducers. One is to turn on the loader. The another is to turn off. How simple is that.
(Of course we can put those two reducers into one reducer that accepts an argument of status like turn(0|1). Doing this we have to first check if the status is either 0 or 1, write an exception handler in case which the argument is not just integer 0 or 1, but also, even worse, string, object, array, null, and undefined. I conclude it is much easier to make two reducers.)
The newly created slice have the actions "reduced" from our reducers, and we export them so that we can call those actions upon the calling of dispatch.
The selectLoadingbarStatus is a callback function passed to useSelector to extract the data of loader from the RootState.
Finally, the slice exports reducer that will be passed to the store.
(The file name that contains slice that exports reducer as default seems not good practice.)
Creating store
The store is a heart of Redux—I believe. The heart of anything is important. As important as it is, usually the system of which is so complex that many people cannot grasp. With Redux Tool, it is not complicated to create that heart.
The store file of Redux is shown below:
./src/app/store.ts
import { configureStore } from "@reduxjs/toolkit";
import loaderReducer from '../features/loader/loaderSlice';
const store = configureStore({
reducer: {
loader: loaderReducer
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;
For many cases, you only have to register your reducers to reducer property of data passed to configureStore.
Two types exported RootState and AppDispatch, unique to your project, are for type hinting across the application.
Then, export the newly created store.
For more detail about the API of configureStore, see Redux Toolkit documentation.
Creating hooks
Let’s create two hooks:
./src/app/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
This is optional. These two functions are just a typed version of useSelector and useDispatch. Instead of using useSelector and useDispatch, we use useAppSelector and useAppDispatch to enable us omit repetitive type scripting.
This approach is after the Redux official documentation Define Typed Hooks.
React
Creating Loader component
The loader component gets its own status data from Redux store. To do so we use the selectLoadingbarStatus callback created earlier. The component shows its HTML element only when the status is 1. Other than that, it shows nothing.
The implementation is as follows:
./src/features/loader/Loader.tsx
import { useAppSelector } from "../../app/hooks";
import { selectLoadingbarStatus } from "./loaderSlice";
import "./loader.css";
export default function Loadingbar() {
const status = useAppSelector(selectLoadingbarStatus);
if (status === 1) {
return <div className="loader"></div>;
} else {
return null;
}
}
The simple loading animation is achieved with a little CSS:
./src/features/loader/loader.css
.loader {
position: fixed;
left: 100%;
top: 0;
width: 100%;
height: 4px;
animation: loader .6s linear infinite;
background-color: dodgerblue;
z-index: 9999;
}
@keyframes loader {
0% { left: -100%; }
100% { left: 100%; }
}
Add Provider
Let’s add a Redux’s Provider component to the root of our React application.
./src/index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import store from "./app/store";
import App from "./App";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Put it all together
Lastly, let’s add our loader component to the main App component and a button that turns on the loader on click.
./src/App.tsx
import Loader from "./features/loader/Loader";
import { useAppDispatch } from "./app/hooks";
import { turnOn, turnOff } from "./features/loader/loaderSlice";
export default function App() {
const dispatch = useAppDispatch();
function handleClick() {
dispatch(turnOn());
// imitating async function
setTimeout(() => {
dispatch(turnOff());
}, 2000);
}
return (
<div className="App">
<Loader />
<br />
<button type="button" onClick={handleClick}>
Load
</button>
</div>
);
}
That’s it.
Run
npm run start
# or
yarn start
The application will run at http://localhost:3000.