Loading State Utility in React

Manage your loaders like a pro

Ankit kaushik
Stackademic

--

Maintaining a flag or having to maintain separate individual state for showing the user some animation while your application is busy fetching some data might be too verbose and cumbersome. What if we could manage several loading states with an effective utility requiring minimal input from the developer.

The Underlying Concept to build upon

Maintain a map to hold the loaderIds (which loader to trigger) and their respective frequency(amount) indicating the number of async operations pending to be completed. Example: If two API calls are happening triggering the same loader (namely `global`) then our map should look like {global: 2} and after both got completed the state should be {global; 0} indicating that the global loader is off now. We could rely upon this state (which we are gonna provide at the root level encouraging single source of truth) to determine the loading state of any loader within the application.

Utility function which could alter this state

function updateLoaders(loaderId: string, amount: number) {
loadersState = {
...loadersState,
[loaderId]: (loaders[loaderId] || 0) + amount,
}
}

Exposing utility function to wrap the promise and update loaders accordingly

function loading<T>(deferFn: () => Promise<T>, loaderId: string) {
updateLoaders(loaderId, 1)
return deferFn().finally(() => {
updateLoaders(loaderId, -1);
});

We should be able to use this function as utility to trigger loading states across the application like:

loading(() => fetch('/api/call'), 'loaderId').then(res => res.json())

Hooking this concept into the React Ecosystem

Now we have laid down the fundamental building blocks (but that’s still scattered ) to construct the utility to manage the Loaders state. Its time to encapsulate all this within the React Ecosystem.

For holding the loaders and their amount (frequency)

const [state, setState] = useState<Record<string, number>>({});

For updating this state

function triggerLoading<T>(deferFn: () => Promise<T>, loaderId: string) {
updateLoaders(loaderId, 1)
return deferFn().finally(() => {
updateLoaders(loaderId, -1);
});
}

function updateLoaders(loaderId: string, amount: number) {
setState(loaders => ({
...loaders,
[loaderId]: (loaders[loaderId] || 0) + amount,
}))
}

Using the context API we need to provide this state (holding the active loaders) and an exposed function to update it. Encapsulating all these:

const LoadersContext = createContext<{loading: LoadingType, state: Record<string, number>}>({});

export function LoadersProvider({ children }: {children: React.ReactNode}) {
const [state, setState] = useState<Record<string, number>>({});
const loading = useCallback(triggerLoading, []);

function triggerLoading<T>(deferFn: () => Promise<T>, loaderId: string) {
updateLoaders(loaderId, 1)
return deferFn().finally(() => {
updateLoaders(loaderId, -1);
});

}

function updateLoaders(loaderId: string, amount: number) {
setState(loaders => ({
...loaders,
[loaderId]: (loaders[loaderId] || 0) + amount,
}))
}

return (
<LoadersContext.Provider value={{state, loading}}>
{children}
</LoadersContext.Provider>
);
}

Provide it at the root:

<LoadersProvider>
<App />
</LoadersProvider>

Also, exposing a couple of abstractions:

function useIsLoading(loaderId: string): boolean {
const {state} = useContext(LoadersContext);
return useMemo(() => state[loaderId] > 0, [loaderId, state]);
}

export default function IsLoading({loaderId, children}: {loaderId: string, children: any}) {
const isLoading = useIsLoading(loaderId);
return isLoading && children;
}

Wrap your loader inside IsLoading providing an identifier

<IsLoading loaderId="myLoaderId">
Loading...
</IsLoading>

and with the help of this final abstraction:

export function useLoading() {
const {loading} = useContext(LoadersContext);
return loading;
}

trigger it like a boss

const loading = useLoading();

loading(() => fetch('api/resource'), 'myLoaderId')
.then(...)

Final version

I hope this might have given you rich insights to building a utility to power up your application, making it easier to manage several loader’s state(on/off) and displaying dynamic skeletons, spinners, shimmers, etc to engage your user while your application fetches the data.

Thanks again, If you like the content please support me by giving your valuable feedback or like(clap). Follow me for more…

Thank you for reading until the end. Please consider following the writer and this publication. Visit Stackademic to find out more about how we are democratizing free programming education around the world.

--

--