Skip to content

Interactivity Router: dispatch navigation DOM events and add state.navigation.loading - #80302

Closed
fabiankaegy wants to merge 2 commits into
trunkfrom
add/interactivity-router-navigation-events
Closed

fabiankaegy wants to merge 2 commits into
trunkfrom
add/interactivity-router-navigation-events

Conversation

@fabiankaegy

Copy link
Copy Markdown
Member

What?

See #70882

This is a prototype for a small piece of the Interactivity Router: a way for code outside the Interactivity API to know when a client-side navigation happens.

It adds two things:

  1. Two native window CustomEvents dispatched by the router: wp-router-navigation-start (right before a navigation begins) and wp-router-navigation-end (once the DOM is patched, the title is updated, and history is updated). Both fire for regular link navigations and for back/forward restores from cache.
  2. A public, documented state.navigation.loading boolean on the core/router store, meant as the replacement for the deprecated state.navigation.hasStarted/hasFinished properties that Luis mentioned in the thread were never meant to be public.

Why?

There are basically two groups of consumers that want to know about client-side navigations:

  1. Interactivity API code that wants navigation state for things like a loading indicator on a Query Loop. That's what hasStarted/hasFinished were used for before they got deprecated, and state.navigation.loading is meant to be their public, documented successor.
  2. Scripts that are not built on the Interactivity API and never will be. Consent managers like OneTrust, tag managers like GTM, analytics, chat widgets. They land on the page as plain script tags, they can't call watch(), and often the snippet comes straight from a vendor so we don't even control it.

For group 2 there wasn't really a way to hook in at all. Turbo has turbo:load, Astro has astro:page-load, htmx has htmx:afterSwap. A plain DOM event is about the smallest possible surface for this: no promises about internals, no state shape to maintain, just "a navigation happened, here's the URL."

How?

Both the event dispatch and the state.navigation.loading flip happen from the same two helper functions, beginNavigation() and endNavigation(), in packages/interactivity-router/src/index.ts. That way the state and the events are always driven from the same transition and can never end up disagreeing with each other.

  • beginNavigation(url) is called once the router has decided a client-side navigation will happen (after the clientNavigationDisabled check passes), before the page is fetched. It sets state.navigation.loading = true and dispatches wp-router-navigation-start with detail: { url }.
  • endNavigation(url, referrer, title) is called after the DOM has been patched, document.title updated, and (for regular navigations) the history entry pushed. It sets state.navigation.loading = false and dispatches wp-router-navigation-end with detail: { url, referrer, title }, where referrer is the URL of the page being navigated away from.

Both the navigate() action and the popstate listener (used for back/forward restores from cache) call these two functions, so the events and the loading state behave consistently no matter how the navigation was triggered.

The deprecated hasStarted/hasFinished getters are untouched and still proxy through to the private core/router/private store exactly like before, so existing code using them keeps working (with the existing deprecation warning) until WP 7.1.

Also updated the package README with a "Reacting to navigation from outside the Interactivity API" section and a description of state.navigation.loading, plus a CHANGELOG entry under Unreleased.

Testing Instructions

  1. Build a page with a Query Loop or any block using data-wp-router-region.
  2. Add a plain script tag (not built on the Interactivity API) that does:
    window.addEventListener( 'wp-router-navigation-start', ( e ) => console.log( 'start', e.detail ) );
    window.addEventListener( 'wp-router-navigation-end', ( e ) => console.log( 'end', e.detail ) );
  3. Click a link that triggers client-side navigation and confirm both events fire in the console with the right url/referrer/title.
  4. Use the browser back/forward buttons and confirm the events fire again for the cached page restore.
  5. From a block's view.js, import @wordpress/interactivity-router and confirm state.navigation.loading is true while a navigation is in flight and false once it settles.

I added e2e coverage in test/e2e/specs/interactivity/router-navigate.spec.ts for both the events and state.navigation.loading, covering a normal navigation and a back/forward restore. I wasn't able to run the e2e suite locally in this environment (wp-env-test needs Docker running, and Docker wasn't reachable here), so I'd appreciate a check that CI is green on these. I did run npm run lint:js on the changed files and tsc -b on the interactivity-router package, both pass clean.

Testing Instructions for Keyboard

No UI changes, nothing keyboard specific to test here.

Open questions

  • If a navigation gets superseded by a newer one before it finishes (e.g. clicking a second link while the first is still fetching), only the winning navigation's wp-router-navigation-end fires. The superseded one gets a start event with no matching end. That felt like the right call to match the existing "only the latest navigation wins" behavior, but flagging it in case folks want something different.
  • Named the two helper functions beginNavigation/endNavigation internally, happy to rename if there's a preferred convention.

Use of AI Tools

This PR was written with Claude Code. I reviewed the implementation, the design decisions around tying the DOM events and state.navigation.loading to the same code path, and the tests before opening this PR.

🤖 Generated with Claude Code

…vigation.loading

Scripts that aren't built on the Interactivity API (consent managers like
OneTrust, tag managers like GTM, analytics, chat widgets) have no way to
react to client-side navigations today. This adds two native window
CustomEvents, wp-router-navigation-start and wp-router-navigation-end, so
those scripts can hook in with a plain window.addEventListener() call.

It also adds a public, documented state.navigation.loading boolean to the
core/router store for code that is built on the Interactivity API, as the
intended replacement for the deprecated state.navigation.hasStarted/
hasFinished properties. Both the events and the state flip are driven from
the same code paths so they can never disagree.

See the discussion in #70882.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the [Package] Interactivity Router /packages/interactivity-router label Jul 15, 2026
@github-actions

Copy link
Copy Markdown

Size Change: +147 B (0%)

Total Size: 7.72 MB

📦 View Changed
Filename Size Change
build/modules/interactivity-router/index.min.js 12.1 kB +147 B (+1.23%)

compressed-size-action

…s e2e tests

The two new navigation-events tests asserted the `wp-router-navigation-end`
event's `title` detail against the block's own rendered heading (e.g.
"Link 1"), but that detail is sourced from `document.title` (the browser
tab title, e.g. "post title – site title"), which is a different string.
Read `document.title` from the page instead of hardcoding the expected
value so the assertion matches what the product code actually reports.

Also confirmed via a controlled local run (reverting to the pre-PR
`index.ts` and rebuilding) that the third failing test in the same CI
shard, "should update the HTML only for the latest navigation", fails
at the same rate on trunk's existing code, so it is pre-existing
flakiness unrelated to this PR and is left untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Flaky tests detected in 77fb97f.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/29401907113
📝 Reported issues:

@luisherranz

Copy link
Copy Markdown
Member

Hey Fabian, I've published an alternative proposal:

I'd love to know what you think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Interactivity Router /packages/interactivity-router

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants