react-router: does not update inside

Version

5.1.2 (not tested on previous versions)

Test Case

https://codesandbox.io/s/react-router-t0ig4

Steps to reproduce

Please see above CodeSandbox.

Expected Behavior

Using React Router with Suspense/lazy.

const Home = () => <div>I am not lazy loaded</div>;
const About = React.lazy( () => import('./About.js') );

const App = () => (
  <Router>
    <div>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
    </div>
    <Suspense fallback="Loading...">
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
      </Switch>
    </Suspense>
  </Router>
);

Home is a normal component, About is lazy-loaded.

When you click “About” link, “Loading…” should appear (it does).

Now, while About is still loading, if you click “Home” link, the page should update immediately and show you the “Home” page again.

Actual Behavior

In fact, the route transition back to “Home” is delayed until About has finished loading.

This is hard to spot in a dev environment, as About will load quickly anyway. So in the CodeSandbox above I’ve substituted a fake Lazy component which never loads. The result is that the page is stuck on “Loading…” forever - navigation ceases to work.

Two things solve the problem:

  1. Add a useLocation() somewhere below the <Switch> element to force <Switch> to re-render on a route transition.
  2. Remove the <Switch> entirely. If it’s just a series of <Route> elements, it works as expected.

I have played around with this a lot, and can’t figure out if it’s a bug in React Router, or in React’s handling of Context within Suspense. But I figured I’d post this here first.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 2
  • Comments: 33 (15 by maintainers)

Most upvoted comments

right back at ya, creator of react router!

Same deal as on Oct 26th. This bug still remains.

Thanks, inventor of React!

I think we fixed this in https://github.com/facebook/react/pull/23095. Try testing it with the @next npm tag of react/react-dom tomorrow.

@ccnklc If I remember right, <Switch> requires its children to each have a path prop, so nesting <Suspense> inside <Switch> won’t work - the <Route>s need to be directly inside <Switch>.

Also, the location of <Suspense> in the tree alters behavior. If you have multiple nested <Switch>es, you might want the Suspense boundary to enclose them all together.

Just tried the original test case with React + ReactDOM 17.0.0-rc.0, and I’m afraid I can confirm the issue remains.

I’ve made a repro case without React Router https://github.com/facebook/react/issues/19701.

While this issue remains outstanding in React, might it be worthwhile implementing a workaround in React Router?

The problem is the use of <Context.Consumer> in Switch - using a useContext() hook solves the problem.

This workaround could be implemented so it only kicks in with versions of React which support hooks. Actual implementation would be more complicated, but something along the lines of:

const {useContext} = React;
if (useContext) useContext( RouterContext );

The point is, that as long as there is any child component of a <Suspense /> component that throws a Promise anything inside the Suspense component will be replaced by the component defined in the fallback prop. When you load a new component inside a switch that is not loaded yet, anything inside the Suspense component will be replaced, even the Switch component. Therefore you have to catch the thrown promise inside the Switch component, meaning you should do something like the following:

<Switch>
  <Route exact path="/">
    <Home />
  </Route>
  <Route path="/about">
    <Suspense fallback={<p>Loading ...</p>}>
      <About />
    </Suspense>
  </Route>
</Switch>

EDIT: I looked at your code sandbox example and I think I get your point now. I am not sure if we have to change anything in the implementation but your expected behaviour seem reasonable to me.

Actually, this is the expected behaviour of <Suspense />. You have to wrap each route inside of an own suspense component.