Skip to content

fix(react-instantsearch-nextjs): refresh results on client-side navigation (#7060)#7063

Draft
Haroenv wants to merge 2 commits into
masterfrom
fix/nextjs-soft-nav-results-7060
Draft

fix(react-instantsearch-nextjs): refresh results on client-side navigation (#7060)#7063
Haroenv wants to merge 2 commits into
masterfrom
fix/nextjs-soft-nav-results-7060

Conversation

@Haroenv

@Haroenv Haroenv commented May 29, 2026

Copy link
Copy Markdown
Contributor

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) made useInstantSearchRouting skip onUpdate() on its first effect run, because on the initial hydration subscribe() already merged the URL into _initialUiState and a redundant setUiState could 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 whatever window[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 useInstantSearchRouting exactly as #6995 left it (so #6980 is fully preserved — the routing hook is unchanged versus master) 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 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 — comparing pathname + 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). A window fallback 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

  • Unit (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 previous onUpdate approach wiped it). Navigation Timing is mocked so the production path is exercised.
  • E2E (softNavigation.test.ts + a /landing fixture page with no InstantSearch): navigating /landing → /Appliances asserts 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

…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>
@codacy-production

codacy-production Bot commented May 29, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 41 complexity

Metric Results
Complexity 41

View in Codacy

TIP This summary will be updated as you push new changes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Comment on lines +90 to +92
if (!isClientNavigation) {
return;
}
@pkg-pr-new

pkg-pr-new Bot commented May 29, 2026

Copy link
Copy Markdown
More templates

algoliasearch-helper

npm i https://pkg.pr.new/algolia/instantsearch/algoliasearch-helper@7063

instantsearch-ui-components

npm i https://pkg.pr.new/algolia/instantsearch/instantsearch-ui-components@7063

instantsearch.css

npm i https://pkg.pr.new/algolia/instantsearch/instantsearch.css@7063

instantsearch.js

npm i https://pkg.pr.new/algolia/instantsearch/instantsearch.js@7063

react-instantsearch

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch@7063

react-instantsearch-core

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch-core@7063

react-instantsearch-nextjs

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch-nextjs@7063

react-instantsearch-router-nextjs

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch-router-nextjs@7063

vue-instantsearch

npm i https://pkg.pr.new/algolia/instantsearch/vue-instantsearch@7063

commit: f72964f

…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>
@Haroenv

Haroenv commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks Copilot — both points were spot on and are addressed:

  1. Pathname-only comparison — the navigation check now compares pathname + search (hash ignored), so a same-path/different-search soft navigation (e.g. /search?q=old/search?q=new) is correctly detected. Covered by a unit test.

  2. Nested <Index> on soft navigation — confirmed real (a soft navigation to a nested-<Index> page wiped the URL with the onUpdate approach). Rather than guard/delay onUpdate, the fix no longer re-runs onUpdate at all: useInstantSearchRouting is left exactly as fix(react-instantsearch-nextjs): preserve URL on mount with nested <Index> #6995 left it, and a new RefreshOnClientNavigation component calls search.refresh() instead. refresh() re-fetches with the current state and never touches the URL, so it can't wipe a nested <Index>. Added a regression test asserting the URL is preserved on a soft navigation to a nested <Index>.

— via Claude

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment on lines +47 to +49
export function isClientNavigation(): boolean {
const currentLocation = window.location.pathname + window.location.search;
return getInitialLocation(currentLocation) !== currentLocation;
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.

[react-instantsearch-next]: Regression: hits not refreshed on client-side navigation (stale results)

2 participants