5

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?

cmpeguerog
  • 537
  • 2
  • 7
  • 12
  • This might give you some ideas: https://blog.maximeheckel.com/posts/preloading-views-with-react – Benjamin Feb 04 '21 at 05:44
  • @Benjamin, thanks, i did try that solution but end up with a complicated, not well tested algorithm to find the components, cuz i use a lot of nesting routes. Im looking for a simpler solution or a very stable library to solve that problem. – cmpeguerog Feb 04 '21 at 15:10

3 Answers3

3

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.

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • Yes concurrent mode is the perfect solucion but is in alpha – cmpeguerog Feb 04 '21 at 15:07
  • @cmpeguerog if you're trying to integrate specifically with suspense, you're already using some of these experimental features already. – Patrick Roberts Feb 04 '21 at 15:36
  • Patric i will give it a try but i think i will wait for stable release for production deploy. Thanks – cmpeguerog Feb 04 '21 at 21:06
  • @PatrickRoberts I tried to use this, but it doesn't seem to work in React 18 with minor updates to the new one. Maybe I'm doing something wrong. [isPending, startTransition] = ... is the only part I changed. Error states isPending is undefined. – bearsworth Jul 11 '22 at 00:26
  • 3
    Here's an updated version that works with react 18 and react-router 6 and also handles browser back button after refresh. https://gist.github.com/perlow/bb7612b25f37667be964f1a1aba42780 – Jonathan Perlow Jul 19 '22 at 18:25
1

For react-router v6:

create SuspenseRouter:

// SuspenseRouter.tsx

import { useLayoutEffect, useRef, useState, useTransition } from 'react'
import { Router } from 'react-router-dom'
import { BrowserHistory, createBrowserHistory, Update } from 'history'

export interface BrowserRouterProps {
  basename?: string
  children?: React.ReactNode
  window?: Window
}

export function SuspenseRouter({ basename, children, window }: BrowserRouterProps) {
  let historyRef = useRef<BrowserHistory>()
  const [isPending, startTransition] = useTransition()

  if (historyRef.current == null) {
    //const history = createBrowserHistory(startTransition, { window });
    historyRef.current = createBrowserHistory({ window })
  }

  let history = historyRef.current
  let [state, setState] = useState({
    action: history.action,
    location: history.location,
  })

  function setStateAsync(update: Update) {
    startTransition(() => {
      setState(update)
    })
  }

  useLayoutEffect(() => history.listen(setStateAsync), [history])

  return (
    <Router
      basename={basename}
      children={children}
      location={state.location}
      navigationType={state.action}
      navigator={history}
    />
  )
}
export default SuspenseRouter

and in App.tsx:

// App.tsx

import React, { Suspense, lazy } from 'react';
import { Routes, Route } from 'react-router-dom';
import SuspenseRouter from "./SuspenseRouter";

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

const App = () => (
  <SuspenseRouter window={window}>
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  </SuspenseRouter>
);
export default App;

*Make sure you have deleted the router you used before

**This answer was given via a link in @Jonathan Perlow's comment. I posted it again because a response is less prominent than an answer and so that there is no need to go to other sites.

shu
  • 139
  • 1
  • 4
0

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();
};
Victor Trusov
  • 1,057
  • 9
  • 19