Search code examples
javascriptreactjsreact-routerreact-router-dom

display Loader while api is fetching data using react-router-dom, defer, useLoaderData


Loader loads for a fraction of a second only, in case there is a delay in API response, I have attached a screen recording for your reference.

Is there any way I can display the loader from the moment the API is called inside of React.Suspense itself? Attached is a screen recording

enter image description here

Following is my source code.

main.jsx

import React, { useRef } from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter, defer, RouterProvider } from "react-router-dom";
import Homepage from "./App";
import "./index.css";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Homepage />,
    loader: async ({ request, params }) => {
      const data = await  fetch('https://api.rapidmock.com/mocks/j4CMM', {
        headers : {
          "x-rapidmock-delay": 2500
        }
      });
      
      return defer({
        results: data.json(),
      });
    },
  },
]);

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

App.jsx

import React, { useState } from "react";
import { useLoaderData, Await, useAsyncValue } from "react-router-dom";
import reactLogo from "./assets/react.svg";
import "./App.css";

const Homepage = () => {
  const { results } = useLoaderData();

  // Render the data when it is available
  return (
    <React.Suspense fallback={<p>Loading data...</p>}>
      <Await
        resolve={results}
        errorElement={<p>Error loading data</p>}
        
      >
        
        {(results) => {
          console.log("results", results);
          return <>Hello</>;
        }}
      </Await>
    </React.Suspense>
  );
};

export default Homepage;

Solution

  • It turns out that if you want to actually defer the loader data then you shouldn't be awaiting anything in the loader function, just return the promise.

    See deferred.

    The "ah-ha" moment came when I read this section in the docs:

    You can literally switch between whether something is going to be deferred or not based on whether you include the await keyword:

    return defer({
      // not deferred:
      packageLocation: await packageLocationPromise,
      // deferred:
      packageLocation: packageLocationPromise,
    });
    

    When the loader is async and awaits anything at all this actually blocks the loader completing and blocks the route's element mounting and rendering. What you are seeing currently is the UI blocked while the fetch occurs, and then the route loads and only the response.json Promise was deferred/awaited.

    Return the entire Promise chain.

    Example:

    loader: ({ request, params }) => {
      const results = fetch("https://api.rapidmock.com/mocks/j4CMM", {
        headers: {
          "x-rapidmock-delay": 2500
        }
      }).then((response) => response.json());
    
      return defer({ results });
    }
    

    Edit display-loader-while-api-is-fetching-data-using-react-router-dom-defer-useload