Skip to content

fix(react-router): symmetric pathname+search url comparison#31153

Open
EricHech wants to merge 1 commit into
ionic-team:mainfrom
EricHech:fix/react-router-search-url-comparison
Open

fix(react-router): symmetric pathname+search url comparison#31153
EricHech wants to merge 1 commit into
ionic-team:mainfrom
EricHech:fix/react-router-search-url-comparison

Conversation

@EricHech
Copy link
Copy Markdown

Issue number: resolves #31152

What is the current behavior?

In IonRouter.handleHistoryChange, the URL-change guard compares mismatched operands:

const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
if (leavingUrl !== location.pathname) { ... }

The left side includes search, the right side does not. For any route with a non-empty query string, the comparison is always unequal, so the transition block runs on every history event — including no-op popstates over same-URL entries pushed via window.history.pushState.

Concretely: when a same-URL history entry on a search-bearing route is popped, action === 'POP' is processed and the IRO transitions to currentRoute.pushedByRoute. The browser URL doesn't change but the rendered view does — the user gets silently teleported to a different page in the stack.

Minimal repro: on any search-bearing route, run in the console:

window.history.pushState({}, '', window.location.href);
window.history.back();

The current behavior swaps the rendered page to the previous entry in locationHistory while the URL stays put. Expected: no visible change.

Full repro and analysis in #31152. This is also the root cause behind the symptom reported in #25534 (framed there as a transition-rerender flash).

What is the new behavior?

The right side of the comparison now also includes search, so the block only runs when the URL actually changed:

const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
const currentUrl = location.pathname + (location.search || '');
if (leavingUrl !== currentUrl) { ... }

Behavior matrix:

  • Search-only navigations (e.g. routerPush(samePath + newSearch)): unchanged — pathname matches but search differs, block still runs.
  • Pathname changes: unchanged — pathnames differ, block still runs.
  • No-op popstates on search-bearing routes: fixed — pathname + search both match, block correctly skipped.

A new Cypress regression test is included (packages/react-router/test/base/tests/e2e/specs/routing.cy.js) that pushes a same-URL state on a search-bearing route, calls history.back(), and asserts the page does not teleport.

Does this introduce a breaking change?

  • Yes
  • No

The comparison becomes stricter (skips the block in more cases than before), but only for the cases where the URL didn't actually change. All cases where the URL did change are still routed through the existing transition logic unchanged.

Other information

Happy to iterate on the fix shape if there's a preferred alternative — e.g. gating behind a flag for backward compat, or restructuring the guard differently. The one-line change above is the smallest possible fix that keeps existing behavior for every "real URL change" case.

The leavingUrl/currentUrl comparison in handleHistoryChange used
`leavingLocationInfo.pathname + leavingLocationInfo.search` on the
left side but `location.pathname` (no search) on the right. For any
route with a non-empty search string, the comparison was always
unequal, so the transition block ran on every history event —
including no-op popstates over same-URL entries (e.g. pushed via
window.history.pushState). That triggered a false POP transition
to the previous route while the browser URL stayed put.

Compares pathname+search on both sides so the block runs only when
the URL actually changed. Verified with a new Cypress regression
that pushes a same-URL state on a search-bearing route, calls
history.back(), and asserts the page does not teleport.
@EricHech EricHech requested a review from a team as a code owner May 19, 2026 07:53
@EricHech EricHech requested a review from BenOsodrac May 19, 2026 07:53
@vercel
Copy link
Copy Markdown

vercel Bot commented May 19, 2026

@EricHech is attempting to deploy a commit to the Ionic Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the package: react @ionic/react package label May 19, 2026
@gnbm gnbm requested a review from ShaneK May 19, 2026 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: react @ionic/react package

Projects

None yet

1 participant