reactjsreact-router-dom

How to check if there's navigation in progress in a react app?


I found out a library named nprogress to show automatically a global progress bar into my react application. To do so I need to listen when it is navigating. I found the hook useNavigation from react-router-dom, which must be used within a RouterProvider but id does not support children. I tried to do this:

import { useEffect } from "react";
import "./App.css";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import HomePage from './HomePage.tsx',
import AboutPage from './AboutPage.tsx'
import {
  Route,
  Link,
  useNavigation,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider,
} from "react-router-dom";


const ProgressBarHandler = () => {
  const navigation = useNavigation();

  useEffect(() => {
    if (navigation.state === "loading") {
      NProgress.start();
    } else {
      NProgress.done();
    }
  }, [navigation.state]);

  return null;
};

const router = createBrowserRouter(
  createRoutesFromElements(
    <>
      <Route path="/" element={<HomePage />} />
      <Route path="/about" element={<AboutPage />} />
    </>
  )
);

function App() {
  return (
    <RouterProvider
      router={router}
      future={{
        v7_startTransition: true,
      }}
      fallbackElement={<ProgressBarHandler />}
    />
  );
}

The thing that I need is to make it start when it is navigating, because sometimes it takes few seconds to finish the render a complex page, and make it stop when it is done. I found out that useNavigation is really cool because it manages automatically also the form actions and many other things.

Do you have any suggestion on how to use this hook or other hooks that keep the navigation monitored?


Solution

  • The router's fallbackElement only renders when the apps routes are initially loading, after that it's no longer used.

    The useNavigation hook can only be used in a component within the router. I suggest creating a root layout route component that renders your ProgressBarHandler (or directly applies the useEffect to call NProgress) and an Outlet for nested routes.

    Examples:

    import { Outlet, useNavigation } from 'react-router-dom';
    
    const ProgressBarHandler = () => {
      const navigation = useNavigation();
    
      useEffect(() => {
        if (navigation.state === "loading") {
          NProgress.start();
        } else {
          NProgress.done();
        }
      }, [navigation.state]);
    
      return null;
    };
    
    const Layout = () => (
      <>
        <ProgressBarHandler />
        <Outlet />
      </>
    );
    
    import { Outlet, useNavigation } from 'react-router-dom';
    
    const Layout = () => {
      const navigation = useNavigation();
    
      useEffect(() => {
        if (navigation.state === "loading") {
          NProgress.start();
        } else {
          NProgress.done();
        }
      }, [navigation.state]);
    
      return <Outlet />;
    };
    

    This alone isn't quite enough to get a loading indicator working.

    The trick to getting a navigation action loading indicator is to:

    1. Add a route loader such that the navigation.state can actually update to the "loading" state value. This is because the loading state is only when

      The loaders for the next routes are being called to render the next page

      The emphasis is mine.

    2. Re-validate each route change. By default route loaders are only called, or re-validated under specific conditions:

      There are several instances where data is revalidated, keeping your UI in sync with your data automatically:

      • After an action is called via:
        • <Form>, <fetcher.Form>, useSubmit, or fetcher.submit
        • When the future.v7_skipActionErrorRevalidation flag is enabled, loaders will not revalidate by default if the action returns or throws a 4xx/5xx Response
        • You can opt-into revalidation for these scenarios via shouldRevalidate and the actionStatus parameter
      • When an explicit revalidation is triggered via useRevalidator
      • When the URL params change for an already rendered route
      • When the URL Search params change
      • When navigating to the same URL as the current URL

      See shouldRevalidate for details. Again, the emphasis is mine.

    Put this all together and you have something like the following:

    const router = createBrowserRouter(
      createRoutesFromElements(
        <Route
          element={<Layout />}
          shouldRevalidate={() => true} // <-- revalidate each route change
          loader={() => null}           // <-- loader to "revalidate"
        >
          <Route path="/" element={<HomePage />} />
          <Route path="/about" element={<AboutPage />} />
        </Route>
      )
    );
    
    function App() {
      return <RouterProvider router={router} />;
    }
    
    Note: I removed the `v7_startTransition` future flag as this appears to interfere with route transitions and the `NProgress` UI you want to use. This flag is meant for:

    This uses React.useTransition instead of React.useState for Router state updates. View the CHANGELOG for more information.

    Demo

    Edit condescending-bose-gnlnx6