InstantSearch renders null after repeated App Router navigation (deferred dispose not restarted)

Open Beginner friendly
#7,110 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Domain
frontend

Research direction

Start in packages/react-instantsearch-core/src/lib/useInstantSearchApi.tsx and trace the deferred cleanup and re-subscribe branches around cleanupTimerRef. Reproduce the issue with two App Router pages and repeated soft navigation; done means plain InstantSearch continues rendering its search box and results instead of returning null after the disposed instance is reused.

Written by the indexing model from the issue text.

Description

Description

When using React InstantSearch with the Next.js App Router, navigating between two pages that each render <InstantSearch> causes the widget to render nothing (blank search box and results) after a few client-side navigations. <InstantSearch> returns null because search.started is false.

This happens with plain <InstantSearch> (not InstantSearchNext). It reproduces reliably after ~3 soft navigations and does not throw — the subtree just silently disappears.

Root cause

In useInstantSearchApi, dispose() is deferred on unmount via setTimeout(cleanup) and tracked in cleanupTimerRef, so an unmount immediately followed by a re-subscribe (a React "update") cancels the dispose:

// unmount / teardown
cleanupTimerRef.current = setTimeout(cleanup); // cleanup = () => search.dispose()

// re-subscribe
if (cleanupTimerRef.current === null) {
  if (!search.started) { search.start(); forceUpdate(); }
} else {
  // "just an update" — cancel the pending dispose, assume still started
  clearTimeout(cleanupTimerRef.current);
  search._preventWidgetCleanup = false;
}

This assumes the gap between teardown and re-subscribe is a single tick. The Next.js App Router preserves the page component fiber across navigation, so searchRef/cleanupTimerRef survive, but the user spends seconds on another route. By the time they navigate back and React re-subscribes:

  1. the deferred setTimeout(cleanup) has already fired → the reused instance is disposed (started === false), and
  2. cleanupTimerRef.current is still the (already-fired) timer id, i.e. non-null.

So the re-subscribe takes the else branch, calls clearTimeout on a dead timer, and never restarts the disposed instance. <InstantSearch> then renders null forever.

Steps to reproduce

  1. Next.js App Router (tested Next 16, React 19, react-instantsearch 7.33.0).
  2. Two routes, each rendering its own <InstantSearch> (different indexName).
  3. Soft-navigate A → B → A → B a few times (via <Link>).
  4. After ~3 navigations the search box + results vanish; search.started is false.

Proposed fix

Reset cleanupTimerRef and restart the search if the reused instance was already disposed:

} else {
  clearTimeout(cleanupTimerRef.current);
  cleanupTimerRef.current = null;
  search._preventWidgetCleanup = false;
  // The deferred dispose may have already fired during a long navigation
  // round-trip (e.g. App Router preserving the page fiber), leaving the
  // reused instance disposed. Restart it so InstantSearch doesn't render null.
  if (!search.started) {
    search.start();
    forceUpdate();
  }
}

Verified against the reproduction above (headless-Chrome/CDP harness): failing 100% of the time → passing 100%.

InstantSearchNext sidesteps this because its provider sets ssrSearchRef, and the teardown short-circuits (if (serverState?.ssrSearchRef) return;) so the instance is never disposed — but plain <InstantSearch> + App Router hits the bug.

Environment

  • react-instantsearch / react-instantsearch-core: 7.33.0
  • react / react-dom: 19
  • next: 16 (App Router)

Source: packages/react-instantsearch-core/src/lib/useInstantSearchApi.tsx

Dominant language
TypeScript
Stars
4.1k
Forks
554
Avg merge
1d 7h
Merged PRs (30d)
43

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from algolia/instantsearch

All issues in algolia/instantsearch

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.