Search code examples
javascriptreactjsreact-routerreact-router-dom

React Router v6.4+ Data API: Retry loader without displaying error


Using react-router 6.21.1 and the new data api, I have routes setup like:

const routes = [
    {
        element: <RootWrapper/>,
        errorElement: <ErrorWrapper/>,
        children: [
            {path: "/login?", element: <LoginWrapper/>},
            {path: "/logout", element: <Logout/>},
            {
                id: "AuthenticatedRoot",
                element: <AuthenticatedRoot/>,
                children: [
                    // ...
                    {path: "/dashboard", element: <Dashboard/>, loader: fetchDashboard},
                ]
            }
        ]
]

My dashboard view is on display 24/7 and revalidates every 5 minutes. It needs to stay up through up to 5 errors to give at least 30 minutes for releases or other unknown downtimes. It was simple to do when doing manual useEffects for data fetching because I could just handle the error that returned from the fetch request:

await axios.get('dashboard')
    .then(
        response => {setData(response.data)},
        error => {
            setRetries(origRetries => {
                if (origRetries >= 6) {
                    setError(true)
                    setErrorMessage(error.message)
                    return 0
                } else {
                    return origRetries + 1
                }
            })
        }
    )

Is there anyway to do something like this with the new loaders?


Solution

  • You could write your fetchDashboard loader function to close over the retry "state". An implementation may look similar to the following:

    const fetchDashboard = () => {
      let cache = {};
      let retry = 0;
    
      return async () => {
        try {
          const result = await axios.get('dashboard');
    
          // Success, cache result and reset retry state
          cache = result;
          retry = 0;
    
          return result;
        } catch (error) {
          // Failure/error, increment retry count
          retry++;
    
          if (retry > 5) {
            // Rethrow error if ran out of retries
            throw error;
          } else {
            // Otherwise return cached result
            return cache;
          }
        }
      };
    };
    
    const routes = [
      {
        element: <RootWrapper />,
        errorElement: <ErrorWrapper />,
        children: [
          { path: "/login?", element: <LoginWrapper /> },
          { path: "/logout", element: <Logout /> },
          {
            id: "AuthenticatedRoot",
            element: <AuthenticatedRoot />,
            children: [
              // ...
              { 
                path: "/dashboard",
                element: <Dashboard />,
                errorElement: <Dashboard />, // <-- render as error element too!
                loader: fetchDashboard(),    // <-- invoke outer function
              },
            ]
          }
        ],
      },
    ];
    
    import {
      useRouteError,
      useLoaderData,
      useNavigate,
      useNavigation
    } from "react-router-dom";
    
    const Dashboard = () => {
      const navigate = useNavigate();
      const { state } = useNavigation(); // "idle"/"loading", could be handy
    
      const data = useLoaderData();
      const error = useRouteError();
    
      // Effect to trigger route revalidation, 5-minute interval
      useEffect(() => {
        const timer = setInterval(() => {
          navigate(".", { replace: true });
        }, 1000 * 60 * 5);
        return () => clearInterval(timer);
      }, []);
    
      ...
    
      return (
        ...
      );
    };
    

    Demo

    Edit react-router-v6-4-data-api-retry-loader-without-displaying-error