React lazy/Suspens + React Router dont change route until component is fetched

Viewed 1333

I am developing an application that uses the default React code spltting using the Lazy/Suspense approach and the React Router for component rendering. Currently, when I navigate to another path, if the network speed is slow, the path is updated and the fallback component is rendered while the component is fetched, is there any way to wait on the current path until the component package is completely downloaded?

2 Answers

Yes, in concurrent mode, where useTransition() is enabled, you can create a custom router to wrap each of the navigation methods on your history object in a suspense transition:

import { useState, unstable_useTransition as useTransition } from 'react';
import { Router } from 'react-router-dom';

const SuspenseRouter = ({ children, history, ...config }) => {
  const [startTransition, isPending] = useTransition(config);
  const [suspenseHistory] = useState(() => {
    const { push, replace, go } = history;

    history.push = (...args) => {
      startTransition(() => { push.apply(history, args); });
    };
    history.replace = (...args) => {
      startTransition(() => { replace.apply(history, args); });
    };
    history.go = (...args) => {
      startTransition(() => { go.apply(history, args); });
    };
  });

  suspenseHistory.isPending = isPending;

  return (
    <Router history={suspenseHistory}>
      {children}
    </Router>
  );
};

export default SuspenseRouter;

Example usage might look something like this:

import { Suspense, lazy, unstable_createRoot as createRoot } from 'react';
import { Switch, Route } from 'react-router-dom';
import { createBrowserHistory } from 'history';
import SuspenseRouter from './components/SuspenseRouter';

const history = createBrowserHistory();

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

const App = () => (
  <SuspenseRouter history={history} timeoutMs={2000}>
    <Suspense fallback="Loading...">
      <Switch>
        <Route path="/" exact={true} component={Home} />
        <Route path="/about" component={About} />
      </Switch>
    </Suspense>
  </SuspenseRouter>
);

createRoot(document.getElementById('root')).render(<App />);

Set timeoutMs to Infinity if you want to wait indefinitely on the previous route. In the example above, setting it to 2000 should wait on the previous route for up to 2 seconds, then display the fallback if the code for the requested route hasn't downloaded by then.

Here is another option: instead of suspending url change you can suspend screen change.

Package react-router-loading allows to show loading bar and fetch some data before switching the screen.

Just use Switch and Route from this package instead of react-router-dom:

import { Switch, Route } from "react-router-loading";

Add loading props to the Route where you want to wait something:

<Route path="/my-component" component={MyComponent} loading/>

And then somewhere at the end of fetch logic in MyComponent add loadingContext.done();:

import { LoadingContext } from "react-router-loading";
const loadingContext = useContext(LoadingContext);

const loading = async () => {
    //fetching some data

    //call method to indicate that fetching is done and we are ready to switch
    loadingContext.done();
};
Related