Skip to content

Fix router.url.query going stale after history.replaceState#6625

Merged
masenf merged 3 commits into
mainfrom
claude/gifted-darwin-u2xff0
Jun 9, 2026
Merged

Fix router.url.query going stale after history.replaceState#6625
masenf merged 3 commits into
mainfrom
claude/gifted-darwin-u2xff0

Conversation

@masenf

@masenf masenf commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Description

Fixes #6603: State.router.url.query and other router_data fields were going stale when the URL was modified via window.history.replaceState or pushState (e.g., through rx.call_script).

Root cause: React Router's useLocation hook does not observe direct history manipulation. The frontend was reading the mirrored location from React Router instead of the live window.location, causing the backend to receive stale query parameters on subsequent events.

Solution: When populating router_data in applyEvent, read the live window.location.search and window.location.hash instead of relying on React Router's location object. The pathname remains basename-relative (from React Router) to avoid double-applying the frontend path prefix. In embed mode, the mirrored location is used since the host page's window.location is unrelated to the in-widget memory router.

Changes

  1. packages/reflex-base/src/reflex_base/.templates/web/utils/state.js: Modified applyEvent to read live query string and hash from window.location when router_data is empty, ensuring synchronization with direct history manipulation.

  2. tests/integration/tests_playwright/test_router_query.py: Added comprehensive integration test covering both dev and prod modes. The test verifies that:

    • Query parameters set via history.replaceState are correctly reflected in State.router.url.query
    • Multiple sequential replaceState calls keep the router in sync
    • The query_parameters dict is properly populated
  3. news/6603.bugfix.md: Added changelog entry.

Test Plan

The new integration test (test_replace_state_syncs_router_query) covers the fix in both dev and prod modes via app_harness_env parametrization. It verifies that:

  • Initial state has no query parameters
  • After history.replaceState("?name=test"), reading the query returns "name=test"
  • After a second replaceState("?name=other&page=2"), the query is updated correctly

All existing tests continue to pass.

https://claude.ai/code/session_01JwZCEa2bkfK9QDp4PP84d8

React Router's location (mirrored in locationRef) does not observe direct
window.history.pushState/replaceState calls (e.g. via rx.call_script), so
State.router.url.query went stale after the URL query was updated that way.

Populate router_data's query string and hash from the live window.location
outside embed mode, while keeping the basename-relative pathname from React
Router so frontend_path is not applied twice. Embedded (mount target) apps
continue to use the in-widget memory router.

Fixes #6603

https://claude.ai/code/session_01JwZCEa2bkfK9QDp4PP84d8
@masenf masenf requested a review from a team as a code owner June 8, 2026 17:36
@codspeed-hq

codspeed-hq Bot commented Jun 8, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing claude/gifted-darwin-u2xff0 (d28d49a) with main (31f785e)

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@greptile-apps

greptile-apps Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes stale router.url.query after window.history.replaceState/pushState by reading window.location.search and .hash directly in applyEvent instead of relying on React Router's mirrored locationRef, which does not observe direct history manipulation. The fix is two lines in state.js; embed mode is preserved by gating on env.MOUNT_TARGET.

  • state.js: Introduces liveLoc — in normal mode this is window.location; in embed mode it remains the in-widget mirrored ref. pathname continues to come from React Router to avoid double-applying the frontend path prefix.
  • test_router_query.py: Adds three integration scenarios (replaceState-not-reactive, redirect push, redirect replace) parametrized over dev and prod modes, covering the fix and documenting the agreed navigation semantics.

Confidence Score: 5/5

Safe to merge — the JS change is minimal and surgical, touches only the two lines that build search/hash in router_data, and the embed-mode guard is well-tested against the existing locationRef contract.

The change is two lines swapping React Router's mirrored location for window.location (with an embed-mode gate). The pre-existing fallback logic and the fact that pathname is intentionally left on the React Router ref both mean the change is well-contained and non-breaking. The new integration test exercises the exact code path in both dev and prod modes.

No files require special attention.

Important Files Changed

Filename Overview
packages/reflex-base/src/reflex_base/.templates/web/utils/state.js Targeted fix: reads live window.location.search/.hash instead of React Router's stale mirrored location when building router_data; embed mode correctly keeps using the mirrored ref
tests/integration/tests_playwright/test_router_query.py New integration test covering replaceState-not-reactive semantics, rx.redirect push, and rx.redirect(replace=True); well-structured with dev/prod parametrization via app_harness_env
packages/reflex-base/news/6625.bugfix.md Changelog entry describing the bug, root cause, and known limitation (pathname not synced via replaceState)

Reviews (2): Last reviewed commit: "test: codify router query sync and redir..." | Re-trigger Greptile

The changed source lives in packages/reflex-base, so the towncrier
fragment must live under packages/reflex-base/news/ and be named by PR
number for the per-package changelog check.

https://claude.ai/code/session_01JwZCEa2bkfK9QDp4PP84d8
Comment thread tests/integration/tests_playwright/test_router_query.py Outdated
Rework the router_query integration test per review feedback:

- Use rx.var computed vars (router.url.query / query_parameters) rendered
  in the app, plus on_load and ping counters, to observe behavior directly.
- Codify that a direct rx.call_script(history.replaceState) is NOT a
  navigation: it fires no event and no on_load, and the router only reflects
  the new URL on the next event sent to the backend.
- Codify rx.redirect(target): client-side push navigation that fires on_load
  and updates the router reactively; back returns to the prior entry.
- Codify rx.redirect(target, replace=True): same reactive behavior but
  replaces the current history entry (back skips it).

Also clarify the news fragment: history mutation is intentionally not
reactive; rx.redirect(replace=True) is the reactive path.

https://claude.ai/code/session_01JwZCEa2bkfK9QDp4PP84d8
@masenf masenf merged commit 985e694 into main Jun 9, 2026
106 checks passed
@masenf masenf deleted the claude/gifted-darwin-u2xff0 branch June 9, 2026 17:15
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

Successfully merging this pull request may close these issues.

Reflex 0.9: self.router.url.query not refreshed when URL is updated with window.history.replaceState

3 participants