fix(react-instantsearch-nextjs): refresh results on client-side navigation (#7060)#7063
fix(react-instantsearch-nextjs): refresh results on client-side navigation (#7060)#7063Haroenv wants to merge 2 commits into
Conversation
…ation (#7060) #6995 stopped the routing effect from calling `onUpdate` on its first run to avoid wiping the URL with a nested `<Index>` (#6980). But on a client-side navigation the App Router remounts `InstantSearchNext`, so that mount also looks like a first run and `onUpdate` was skipped — no fresh search ran and the page kept whatever `window[InstantSearchInitialResults]` held (empty or stale) until a full reload. Tell a genuine initial hydration apart from a client-side navigation using the Navigation Timing API: `performance.getEntriesByType('navigation')[0].name` holds the URL the document was hard-loaded with and is unaffected by SPA navigations. When the current path differs from it, the mount is a client-side navigation and we run `onUpdate` to refresh the results; when it matches, it is the initial hydration and we skip `onUpdate`, preserving #6980. This also covers the case where the navigation lands on the first InstantSearch page of the session (e.g. coming from a page without InstantSearch). The comparison uses `window.location.pathname` so it stays consistent when a `basePath` is set (`usePathname()` strips it). A `window` fallback handles environments without Navigation Timing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 41 |
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Pull request overview
This PR updates react-instantsearch-nextjs routing to refresh InstantSearch results after Next.js App Router client-side navigation while preserving the initial-hydration behavior added for nested <Index> URL preservation.
Changes:
- Adds initial-document path detection in
useInstantSearchRouting. - Adds unit tests for remount behavior after client-side navigation.
- Adds an E2E fixture and test covering navigation from a non-InstantSearch page.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
packages/react-instantsearch-nextjs/src/useInstantSearchRouting.ts |
Adds first-load vs client-navigation detection before deciding whether to run onUpdate. |
packages/react-instantsearch-nextjs/src/__tests__/useInstantSearchRouting.test.tsx |
Adds mocked navigation timing and remount-focused routing tests. |
packages/react-instantsearch-nextjs/__tests__/e2e/softNavigation.test.ts |
Adds Playwright coverage for soft navigation into an InstantSearch page. |
examples/react/next-app-router/app/landing/page.tsx |
Adds a plain landing page fixture used by the E2E test. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // comparison is consistent with the Navigation Timing URL when a | ||
| // `basePath` is configured (`usePathname()` strips it, the others don't). | ||
| const currentPath = window.location.pathname; | ||
| const isClientNavigation = getInitialPath(currentPath) !== currentPath; |
| if (!isClientNavigation) { | ||
| return; | ||
| } |
More templates
algoliasearch-helper
instantsearch-ui-components
instantsearch.css
instantsearch.js
react-instantsearch
react-instantsearch-core
react-instantsearch-nextjs
react-instantsearch-router-nextjs
vue-instantsearch
commit: |
…f re-running onUpdate Address review feedback on the soft-navigation fix. The previous approach re-ran the router's `onUpdate` on a client-side navigation's first render. As pointed out in review, that re-introduced the #6980 failure mode on soft navigation: `onUpdate` writes the URL, and on a nested `<Index>` page whose children only register on a second render pass it serialized incomplete state and wiped the URL. It also only compared the pathname, missing same-path/different-search navigations. Instead, leave `useInstantSearchRouting` exactly as #6995 left it and add a small client-only `RefreshOnClientNavigation` component that calls `search.refresh()` when it detects a client-side navigation. `refresh()` re-fetches with the already-correct UI state and never touches the URL, so it fixes #7060 without any #6980 risk. Navigation detection uses the Navigation Timing API (`pathname` + `search`, hash ignored), with a `window` fallback for environments without it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks Copilot — both points were spot on and are addressed:
— via Claude |
| export function isClientNavigation(): boolean { | ||
| const currentLocation = window.location.pathname + window.location.search; | ||
| return getInitialLocation(currentLocation) !== currentLocation; |
Summary
Fixes #7060. After #6995, server-rendered results stopped being reflected on client-side (soft) navigation in the Next.js App Router — the destination page showed empty or stale hits until a full reload.
Root cause
#6995 ("preserve URL on mount with nested
<Index>", fixing #6980) madeuseInstantSearchRoutingskiponUpdate()on its first effect run, because on the initial hydrationsubscribe()already merged the URL into_initialUiStateand a redundantsetUiStatecould wipe the URL with a nested<Index>(whose children only register on a second render pass).But on a client-side navigation the App Router remounts
InstantSearchNext, so the new instance's first run looks identical to a first hydration —onUpdate()was skipped, no fresh search ran, and the remounted instance rendered only whateverwindow[InstantSearchInitialResults]held: empty when arriving from a page without InstantSearch, or stale otherwise. A hard reload worked because the document re-injects correct initial results.Fix
Leave
useInstantSearchRoutingexactly as #6995 left it (so #6980 is fully preserved — the routing hook is unchanged versusmaster) and add a small client-onlyRefreshOnClientNavigationcomponent that callssearch.refresh()when it detects a client-side navigation.refresh()re-fetches with the already-correct current UI state and does not touch the URL, so it fixes #7060 without any risk of the #6980 wipe. Navigation is detected with the Navigation Timing API (performance.getEntriesByType('navigation')[0].name), which holds the URL the document was hard-loaded with and is unaffected by SPA navigations — comparingpathname+search(hash ignored). This also covers the case where the navigation lands on the first InstantSearch page of the session (e.g. coming from a page without InstantSearch). Awindowfallback covers environments without Navigation Timing.The change is purely additive (the routing hook and its tests are untouched).
Tradeoff
Every client-side navigation now re-fetches rather than reusing the streamed SSR results. This restores the pre-#6995 behavior (not a new regression versus before the bug) and is the only safe option: on a soft navigation the injected results may be empty or stale, and a cheap check can't tell a correct injected result from a stale one — so trusting them would re-break the stale case.
Tests
RefreshOnClientNavigation.test.tsx):isClientNavigation()(pathname and search-param sensitivity), refresh is called on a client navigation and not on initial hydration, and the URL is preserved on a soft navigation to a nested<Index>(the InstantSearchNext routing removes query param from url #6980 failure mode, here on soft navigation — verified the previousonUpdateapproach wiped it). Navigation Timing is mocked so the production path is exercised.softNavigation.test.ts+ a/landingfixture page with no InstantSearch): navigating/landing → /Appliancesasserts results render. Verified this fails on the fix(react-instantsearch-nextjs): preserve URL on mount with nested <Index> #6995 build (0 hits) and passes with the fix.All green locally: unit 17/17, e2e 7/7 (incl. existing
backButton/multipleHooks).🤖 Generated with Claude Code