Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-rendering the component for a source route after navigating to a destination route in a promise #506

Open
ElanMedoff opened this issue Jan 27, 2025 · 2 comments

Comments

@ElanMedoff
Copy link

Issue

When navigating from a source route (i.e. /route-a) to a destination route (/route-b) after a promise, the component for the source route is re-rendered after the navigation - when the URL is already set to the destination.

Reproducing

I was able to reproduce this issue in a code sandbox: https://codesandbox.io/p/sandbox/exciting-wilson-l9sk9x

export default function App() {
  return (
    <Router>
      <Switch>
        <Route path="/" component={Root} />
        <Route path="/route-a" component={PageA} />
        <Route path="/route-b" component={PageB} />
      </Switch>
    </Router>
  );
}

function PageA() {
  console.log("PageA render, pathname:", window.location.pathname);
  return (
    <div>
      <p>page A</p>
      <button
        onClick={() => {
          Promise.resolve().then(() => {
            navigate("/route-b");
          });
        }}
      >
        navigate after promise
      </button>
      <button
        onClick={() => {
          navigate("/route-b");
        }}
      >
        navigate immediately
      </button>
    </div>
  );
}

function PageB() {
  return <div>page B</div>;
}

function Root() {
  return (
    <div>
      <h1>Steps to repro</h1>
      ...
    </div>
  );
}
  • Go to /route-a
  • Open the console
  • Click navigate after promise

The console will read:

PageA render, pathname: /route-a
PageA render, pathname: /route-b

This is unexpected, since PageA should only render on /route-a, not /route-b

If you repro with the following steps, the extra render does not occur:

  • Go to /route-a
  • Open the console
  • Click navigate immediately

The console will only read:

PageA render, pathname: /route-a

Which is what I would expect in both cases.

Why this is an issue

Say I have a condition in PageA to check for a search param (i.e. auth=1), and if it does not exist, navigate to an earlier route where the search param is populated. When navigating from /route-a to /route-b, I don't include the search param i.e. I would navigate from /route-a?auth=1 to /route-b , not /route-b?auth=1. When navigating after promise, PageA is re-rendered with the URL of /route-b. Since the auth=1 search param is not present, my condition kicks in, and I'm navigated away from the destination route. This is more or less the scenario that led me to root-cause this issue.

Notes

@cataclyst
Copy link

cataclyst commented Jan 30, 2025

I'm seeing a very similar issue, but in a slightly different constellation - maybe they are related, I'm not sure. Mine happens when you use the browser's back/forward navigation buttons.

You can see this in the following sandbox: https://3xskvp.csb.app/page/1 - please note that, because it seems to be related to the browser's native functionality, you need to observe this in the "deployed" version of the sandbox, not in sandbox.io's preview browser. For study purposes, here's the editable version of the sandbox: https://codesandbox.io/p/sandbox/stupefied-frost-3xskvp

I have two routes, in the example /page/:id and /a-different-page/:id. The associated components print the current useLocation() and useParams() values to the console.

When you switch via the routing links, everything is fine. But if you navigate to a page and then use the browser's back button, the route being left re-renders unexpectedly, having received the new location (like @ElanMedoff describes), but also the old params map from the previous route.

This is unfortunate because in my real world case, I have side-effects in a useEffect hook which shouldn't fire when the component is actually being unmounted. And even if they do, they receive params which don't match what the route/component expects.

No promises involved in my case, so the setup is quite different, but the effect looks similar enough that maybe the problems are related and this could help for analysis.

Edit: FWIW, my case does use createRoot, and the issue still happens (to add to the confusion).

@molefrog
Copy link
Owner

molefrog commented Feb 5, 2025

This sounds like a tearing (reactwg/react-18#69) issue, but I just can't figure out why it happens. Wouter relies on useSyncExternalStore and this hook should be tearing-safe. I'm out of ideas for now, but I will keep digging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants