React Router Dom and loader animation

3 min readNov 3, 2023

During the React course task, I encountered an issue with displaying a “loader animation” while fetching data. I had previously achieved this using the useEffect hook for fetching data and the useState hook for toggling the loader animation.

However, with the new features of React Router Dom 6, the process became less straightforward. React Router now offers a new feature called the loader function, which can replace the useEffect for data fetching on page load. Additionally, the error can be handled using the errorElement property.

But, the challenge for me was to display the loading animation because the loader function operates outside the component and cannot be wrapped with useState for the loader animation. After some trial and error, I finally found a working solution for TypeScript.

To enable the router to work, we need to wrap our application in the RouterProvider:

<RouterProvider router={router} />

Router provider demands router property we can create it with createBrowserRouter:

export const router = createBrowserRouter(
<Route path="/" element={<RootLayout />}>
element={<MyMainComponent />}
errorElement={<MyErrorComponent />}

Here we can find the first difference compared to the useEffect approach. I used to utilize useEffect with an async/await fetch function to pass the awaited fetch result to the component. However, in this case, the dataFetchFunction has to be slightly different. Here, I use the defer() function to pass the Promise back to the component. Deferred data is returned immediately, and Promise processing is passed to the component.

export const dataFetchFunction = async ({ params }: fetchParams) => {
const res = fetch(`${params[id]}`})
.then((data) => data.json());
return defer({ data: res });

The last thing is to handle this deferred data inside the component. The useLoaderData() hook returns data from dataFetchFunction. The Suspense component displays the “Loading…” text from the fallback property while the Promise is pending.

Depending on the Promise’s status, the router will call the element or errorElement. If the Promise is fulfilled, the data will be passed to the Await component, and our last component ShowFetchedData will finally display our fetched data using useAsyncValue(). This hook returns the result of the awaited Promise, and here you can work with your data.

export default function MyMainComponent(): JSX.Element {
const { data } = useLoaderData() as { data: Promise<TFetchReturnType> };
const message = handleErrorMessage(useRouteError());
return (
{message && <div>{`Error ${message}`}</div>}
<Suspense fallback={<div>"Loading..." </div>/>}>
<Await resolve={data}>
<ShowFetchedData />

function ShowFetchedData(): JSX.Element {
const { astronomicalObjects, page } = useAsyncValue() as TFetchReturnType;
return (
{data} // Depends on your data type show it as you want

handleErrorMessage() function handles the possible errors, it’s not perfect, but is shows how to handle different types of errors.

export const handleErrorMessage = (error: unknown) => {
let message = '';
if (isRouteErrorResponse(error)) {
message = `${error.status} ${error.statusText}`;
} else if (error instanceof Error) {
message = error.message;
return message;

I found articles about this topic (React Router 6 Deferred Fetch, GitHub discussions), but they are mostly written on JavaScript. And I spent a hours to figure out how to type data in this example. Maybe it is not the best variant, because I am not using type guard, but it works and type guards can be added later.

defer(), useLoaderData, useAsyncValue, useRouteError, Await, isRouteErrorResponse() are imported from react-router-dom.
Suspense is imported from react.




geophysics => construction & design => IT | Getting into web development - better today than never.