🚀 A simple implementation of a Loader in React ⚛️ using [ContextAPI, Custom Hook, and TypeScript]

Prithviraj Mazumder
TechVerito
Published in
6 min readApr 22, 2024
Photo by Ehimetalor Akhere Unuabona on Unsplash

TLDR 👀

If you are a PRO and only interested in implementation then in this Github Repository you can find the code.

First of all forget that you are a <Developer />
Instead you are an End-User 👩‍💻 of an Application and you have clicked a Button in the UI and nothing happened for a while and suddenly a new element popups up in the screen.

You will have to work your brain 🧠 for some seconds to figure out what happened. Just like the below image👇

You definitely don’t want your Users to work their brain out to use your Application.

You should have a Loader/Spinner to indicate your users that something is happening behind the UI that they can’t see

So there is nothing new in this

Majority of you guys knew this. But then also we sometimes forget to add loading state in our applications, or feel lazy to add multiple loading state in our components

In React ⚛️ we generally have this👇 mess of (useState)s when we implement multiple pending state UI in a component

import { useState } from 'react'

export const SomeComponent = () => {
const [isCarsLoading, setIsCarsLoading] = useState(false)
const [isEngineLoading, setIsEngineLoading] = useState(false)

const fetchCars = () => {
try {
setIsCarsLoading(true)
...
} catch (error) {
...
} finally {
setIsCarsLoading(false)
}
}

const fetchEngineByCarModel = (carModel: string) => {
try {
setIsEngineLoading(true)
...
} catch (error) {
...
} finally {
setIsEngineLoading(false)
}
}

By this above implementation we need to always declare a separate loading state and conditionally render a Loader/Spinner for the showing pending state in UI

I think it will be great ✨ if we have some function to Start/Stop a Loader from any where in the Component Tree and it will show a generic loader.

And if we want something other than the generic loader then only we will add a separate loading states in components

So our implementation of the Generic Global Loader’s Architechture will look like this 👇

Loader Context Architecture

Let’s try to code this </>

Step 1: At first create the LoaderContext and it’s Type

//LoaderProvider.ts

export type LoaderContextProps = {
isLoading: boolean
start: () => void
stop: () => void
}

export const LoaderContext = createContext<LoaderContextProps>({} as LoaderContextProps)

Step 2: Now let’s create a Provider for this Context

//LoaderProvider.ts

export const LoaderProvider = ({ children }: { children: ReactNode }) => {
const [isLoading, setIsLoading] = useState(false)
const [loaderText, setLoaderText] = useState('')

const start = (loaderText = 'Loader...') => {
setLoaderText(loaderText)
setIsLoading(true)
}

const stop = () => setIsLoading(false)

return (
<LoaderContext.Provider
value={{
isLoading,
start,
stop,
loaderText
}}
>
{children}
</LoaderContext.Provider>
)
}

In this Context we have

  • isLoading → (this is a state which trigger the loading state)
  • start (function that will take a loader text optionally and will setIsLoading to true)
  • stop → (function that will setIsLoading to true)
  • loaderText → (this is a state which stores the LoaderText)

Step 3: Create a helper Custom Hook for this context

//LoaderProvider.ts

export const useLoader = () => {
const loaderContext = useContext(LoaderContext)

if (!loaderContext) {
throw new Error('Please use useLoader inside the context of LoaderProvider')
}

return {
start: loaderContext.start,
stop: loaderContext.stop
}
}

Step 4: Now let’s create a <Loader /> Component which will listen to the isLoading State of <Loader/> Context

//Loader.tsx

import { useContext } from 'react'
import { LoaderContext } from '@/stores/LoaderProvider.tsx'
export const Loader = () => {
const { isLoading, loaderText } = useContext(LoaderContext)
return (
<>
{isLoading ? (
<div className="h-full w-full fixed top-0 left-0 bg-black/20 z-[99999]">
<div className="fixed top-1/2 -translate-x-1/2 left-1/2 -translate-y-1/2 flex flex-col items-center gap-4">
<span className="loading loading-spinner loading-lg text-primary" />
<span className="text">{loaderText}</span>
</div>
</div>
) : null}
</>
)
}

Now Let’s implement this Global <Loader/> Component on a Page

// Employees.tsx

import { useLoader } from '@/stores/LoaderProvider.tsx'
import { useEffect, useState } from 'react'
import { Employee } from '@/constants/employees.ts'
import { fetchEmployees } from '@/apis/employees.ts'

export const EmployeesList = () => {
const [employees, setEmployees] = useState<Array<Employee>>([])
const loader = useLoader()
const getEmployees = async () => {
try {
loader.start()
const employees = await fetchEmployees()
setEmployees(employees)
} catch (e) {
alert('Employees cannot be fetched!')
} finally {
loader.stop()
}
}
useEffect(() => {
void getEmployees()
}, [])
if (!employees?.length) {
return <></>
}
return (
<>
<h1 className="text-3xl font-bold mb-4">Employees</h1>
<button className="btn btn-primary btn-sm" onClick={getEmployees}>
Re-fetch
</button>
<div className="overflow-x-auto mt-8">
<table className="table w-full">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
</tr>
</thead>
<tbody>
{employees.map((employee, index) => (
<tr key={employee.id}>
<th>{index + 1}</th>
<td>{employee.name}</td>
<td>{employee.email}</td>
<td>{employee.phone}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)
}

Voila 🌟, now we can have loader state which we can trigger from anywhere without any extra loading state in your component

But we have a problem with this implementation 😢

Since we have a single Loading State in the Context and multiple components will toggle this loader text

There will be a situation where a request named requestA that takes 300ms to execute
And simultaneously we have another request named requestB which takes 200ms to execute

And if both the requests calls loader.start() at start of execution then the loader will start at same time
but will stop when the requestB stops executing while requestA is still in the progress of execution.

So we won’t have a spinner for the entire execution time of requestA

Solution 🌟

We will introduce a loader stack where loader.start() will insert a value in the stack and loader.stop() will delete a value from the stack

  • So if the stack is not empty then the value of isLoading will be true
  • And if the stack is empty then the value of isLoading will be false
Loader Stack

Now let’s change the implementation in ContextProvider

// LoaderProvider.ts

import { createContext, ReactNode, useContext, useEffect, useState } from 'react'

export type LoaderContextProps = {
isLoading: boolean
loaderText: string
start: (loaderText?: string) => void
stop: () => void
}

export const LoaderContext = createContext<LoaderContextProps>({} as LoaderContextProps)

export const LoaderProvider = ({ children }: { children: ReactNode }) => {
const [isLoading, setIsLoading] = useState(false)
const [loaderText, setLoaderText] = useState('')
const [loaderStack, setLoaderStack] = useState<Array<boolean>>([])

const start = (loaderText = 'Loader...') => {
setLoaderText(loaderText)
setLoaderStack([...loaderStack, true])
}

const stop = () => setLoaderStack([...loaderStack.slice(1)])

useEffect(() => {
if (!loaderStack.length) {
setIsLoading(false)
return
}
setIsLoading(true)
}, [loaderStack, start, stop])

return (
<LoaderContext.Provider
value={{
isLoading,
start,
stop,
loaderText
}}
>
{children}
</LoaderContext.Provider>
)
}

export const useLoader = () => {
const loaderContext = useContext(LoaderContext)

if (!loaderContext) {
throw new Error('Please use useLoader inside the context of LoaderProvider')
}

return {
start: loaderContext.start,
stop: loaderContext.stop
}
}

Wrapping Up

Building scalable and easy-to-use components is every developer’s dream. Feel free to use and tweak them in your projects, and remember happy coding, React developers! 😎✨

Feedback ✉️

Please let me know if you have some suggestions on it and feel free to fork it from this repo and change it however you want.

--

--

Prithviraj Mazumder
TechVerito

A Full-stack developer @TechVerito, developing robust and scalable softwares, and clean code for my team