instantsearch: Instantsearch hooks: router not getting along with Next.JS

We are trying out a server rendered search implementation with NextJS. All seems okay until we try to add a routing object to InstantSearch. We are using routing to have urls for our search state, and to make them SEO friendly (q=potato&type=tuber). There are all sorts of quirks though, from additional re-renders on load to the rest of the application routing breaking.

1. Re-Render Example: If we try starting at url /search?q=potato we immediately see a re-render to /search.

2. Routing Example: When we click on a hit/result <Link href={/post/${hit.slug}>šŸ„” hit</Link> we are taken to our expected /post/${hit.slug} url, but then from there our routing in general seems to be broken. Clicking back moves to /search?q=potato, but only the url changes. Page content is not updated.

StepBrothers_3lg copy

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 35
  • Comments: 87 (29 by maintainers)

Commits related to this issue

Most upvoted comments

Need this. Iā€™m pretty frustrated after 2 years of developing nextJS applications with algolia that this hasnā€™t been addressed.

Algolia team, just want to chime in here in the event anyone is listening to this thread. Native support for this issue really critical. Can you please provide an update on where this fix is on the roadmap and when it can be expected, if ever?

@dhayab do we have an ETA for the native support solution? by that I mean 3rd party routing going along with Algolia InstantSearch

Maybe to have like an example working with NextJS, including SSG, SSR and CSR. The user flow would be like that land on a category (SSG) => facet click to filtered category(SSR) => and for example change sorting (CSR)

Itā€™s a temporary solution until we finish implementation and ship native support for third-party router later this year.

Algolia, when will the solution be released? No one is responsible? Hey, it is something criticalā€¦

Hi @Haroenv thanks for the response. And yes, no doubt, def not a simple problem keeping the state and router in sync. Its feels so close šŸ ! Iā€™ve forked your sandbox to demonstrate item 2 (Routing example above)ā€¦

https://codesandbox.io/s/hooks-next-bug-3506-item-2-6ekug6?file=/pages/index.tsx

To reproduceā€¦

  1. perform a search. either a query or refinement ā€“ anything that triggers a URL change.
  2. Then click on one of the results where you will be routed to ā€˜/testā€™
  3. The click back, the url will change but you will be stuck on the test page

Perhaps issue is something to do with how the InstantSearch component / router is unmounted.

Hi everyone ! šŸ™‹ā€ā™‚ļø

First of all, thank you for your patience and for providing precious feedback.

Our solution for creating a Next.js compatible router for InstantSearch is now generally available and published on npm as react-instantsearch-hooks-router-nextjs. It has to be a separate package as it has next as a peerDependency.

Its documentation is available on the Algolia docs right there : https://www.algolia.com/doc/api-reference/widgets/instantsearch-next-router/react-hooks/

You can also see it in action on CodeSandbox : https://codesandbox.io/s/github/algolia/instantsearch/tree/master/examples/react-hooks/next-routing

If youā€™re seeing any problem still, please raise another issue with a reproduction and make sure to check that itā€™s not just related to back button press, try to see if reloading the page raises the same issue. If youā€™re also on a dynamic route, check that the issue is not caused by components/hooks being unmounted.

Thanks again to all of you !

Any progress on a native solution? Thanks!

Hi @francoischalifour , iā€™m still experiencing the 2 issues mentioned by @dsbrianwebster (iā€™m using nextjs 12 & react 17), especially the broken back button

Hi all, we are currently working on a solution, and we came up with the idea of a package that lets you create a custom router that is compatible with Next.js and fixes the back button issue as well as the dynamic routing issue.

It needs to be its own package because it has next as a peerDependency so we can use its router.

Before moving forward weā€™d appreciate getting your feedback. For this we released this experimental package instantsearch-router-next-experimental Please note that as its name implies, this one should only be used for testing. If the solution is validated a new packaged with a definitive name and API will get published.

To use it, install it :

yarn add instantsearch-router-next-experimental
# or if you're using npm
npm install instantsearch-router-next-experimental

Then you can instantiate it and pass it in the routing prop of the <InstantSearch> component :

import { createInstantSearchNextRouter } from 'instantsearch-router-next-experimental';

export default function Page({ serverState, url }) {
  return (
    <InstantSearchSSRProvider {...serverState}>
      <InstantSearch
        searchClient={searchClient}
        indexName="instant_search"
        routing={{ router: createInstantSearchNextRouter({ serverUrl: url }) }}
      >
        {/* ... */}
      </InstantSearch>
    </InstantSearchSSRProvider>
  );
}

If you are not using SSR therefore not having a url prop, you can simply call createInstantSearchNextRouter without any argument. And if you have custom routing methods such as createUrl or parseUrl you may pass them in the object argument as well. You can find more info about usage on the npm page

You may find the source code in the package itself or on this branch.

Weā€™re looking forward to hearing how it works for you. It can be a reaction to this comment if it works nicely for you, or a comment detailing which version of next and react-instantsearch-hooks you are using if it does not work !

Thanks šŸ˜

Thanks @dhmacs, I used that workaround for the moment, triggering a page reload once the popstate occurs over the search page. Something simple like this for the moment, until issue is fixed:

useEffect(() => {
    const handleRouteChange = () => {
      if (window.location.href.includes('${searchPage}')) window.location.reload();
    };
    window.addEventListener('popstate', handleRouteChange);
    return () => {
      window.removeEventListener('popstate', handleRouteChange);
    };
  }, []);

@Raesta : we want to gather as much feedback as possible to make sure it covers most issues and versions, so it should come out in around 1 month from now

@colis : I canā€™t seem to reproduce the issue on my end, there might be some specific routing ? A reproduction of the issue on codesandbox would help greatly

Hi @wenche, thanks again for the replies ! I totally agree, weā€™ll further iterate on how InstantSearch cleans up after unmounting, as this problem could impact other middlewares as well. It should be good by the time the official package is released šŸ˜ƒ

@joshgeller Sorry for the lack of clarity, what I meant is that it would be nice to make that onUpdate part of the api.

FYI: Iā€™ve copied the original history.ts into my repo and made some adjustments in order to use the nextjs router instead - it certainly hasnā€™t been cleaned up but feel free to take a look here: https://gist.github.com/klaasman/b0e06e8f63dbae70829424232285570c

@colis : After some intense debugging session I found out that when reloading the page with the initial query, removeWidgets gets called on the taxonomies.auction_tax.name refinement list which makes InstantSearch cleanup the state thus resetting the query. So you have to make sure your <RefinementList> or useRefinementList is mounted once and not unmounted later. It most likely doesnā€™t have anything to do with Next.js or the router. Thereā€™s something important to know when using React InstantSearch Hooks, itā€™s that unmounting a widget component or hook cleans up its state. Which means if you do conditional rendering you have to be very careful. Thereā€™s a guide for you to know more about it : https://www.algolia.com/doc/guides/building-search-ui/widgets/show-and-hide-widgets/react-hooks/

@osseonews : Itā€™s coming out this month ! Weā€™ll make an announcement on this issue when itā€™s the case šŸ˜ƒ

@osseonews : Yes this is fixed in the package ! Using a simple <a> tag does work but it beats the purpose of making Single Page Apps, for example you will lose the algoliasearch cache so any subsequent back button click will trigger a request to Algolia again.

@Raesta : I will need a reproduction to help please, can be a Codesandbox or a repo. Are you using a custom createURL ? This normally shouldnā€™t ever return null.

Hi, Iā€™m also still experiencing issue number 2, mentioned by @dsbrianwebster (iā€™m using nextjs 12 & react 18). Has anyone come up to a solution? The custom router implementation maybe? Thanks

I also have this warning:

dev: warn  - ../../node_modules/react-instantsearch-hooks-server/dist/es/getServerState.js
dev: Critical dependency: the request of a dependency is an expression

And when I switch pages in Next like this:

  • Homepage -> no algolia
  • Category page -> has algolia
  • To homepage -> no algolia

Its crashing in the instantsearch clean up:

TypeError: Cannot read properties of null (reading 'state')

Call Stack
eval
../../node_modules/instantsearch.js/es/widgets/index/index.js (530:0)
Array.forEach
<anonymous>
Object.dispose
../../node_modules/instantsearch.js/es/widgets/index/index.js (520:0)
eval
../../node_modules/instantsearch.js/es/widgets/index/index.js (254:0)
Array.reduce
<anonymous>
Object.removeWidgets
../../node_modules/instantsearch.js/es/widgets/index/index.js (252:0)
InstantSearch.removeWidgets
../../node_modules/instantsearch.js/es/lib/InstantSearch.js (357:0)
InstantSearch.dispose
../../node_modules/instantsearch.js/es/lib/InstantSearch.js (494:0)
eval
../../node_modules/react-instantsearch-hooks/dist/es/lib/useInstantSearchApi.js (40:0)
safelyCallDestroy
../../node_modules/react-dom/cjs/react-dom.development.js (22932:0)
commitHookEffectListUnmount
../../node_modules/react-dom/cjs/react-dom.development.js (23100:0)
commitPassiveUnmountInsideDeletedTreeOnFiber
../../node_modules/react-dom/cjs/react-dom.development.js (25098:0)
commitPassiveUnmountEffectsInsideOfDeletedTree_begin
../../node_modules/react-dom/cjs/react-dom.development.js (25048:0)
commitPassiveUnmountEffects_begin
../../node_modules/react-dom/cjs/react-dom.development.js (24956:0)
commitPassiveUnmountEffects
../../node_modules/react-dom/cjs/react-dom.development.js (24941:0)
flushPassiveEffectsImpl
../../node_modules/react-dom/cjs/react-dom.development.js (27038:0)
flushPassiveEffects
../../node_modules/react-dom/cjs/react-dom.development.js (26984:0)
eval
../../node_modules/react-dom/cjs/react-dom.development.js (26769:0)
workLoop
../../node_modules/scheduler/cjs/scheduler.development.js (266:0)
flushWork
../../node_modules/scheduler/cjs/scheduler.development.js (239:0)
MessagePort.performWorkUntilDeadline
../../node_modules/scheduler/cjs/scheduler.development.js (533:0)

Here is a snippet of line 530

localWidgets.forEach(function (widget) {
        if (widget.dispose) {
          // The dispose function is always called once the instance is started
          // (it's an effect of `removeWidgets`). The index is initialized and
          // the Helper is available. We don't care about the return value of
          // `dispose` because the index is removed. We can't call `removeWidgets`
          // because we want to keep the widgets on the instance, to allow idempotent
          // operations on `add` & `remove`.
          widget.dispose({
            helper: helper,
            state: helper.state, <----- 530
            parent: _this5
          });
        }
      });

package.json:

        "react-instantsearch-hooks-server": "^6.40.0",
        "react-instantsearch-hooks-web": "^6.40.0",
        "react-instantsearch-hooks-router-nextjs": "^6.40.0",

Everything works as expected until we start using sorting. As soon as we use sorting and hit a product the browser back button does not work anymore. Filtering works as expected.

@adesege do you have a reproduction? the example in the repo is using a dynamic path

When using the back button from a page not using InstantSearch to my InstantSearch page, every facet is removed from the url (and from the state). Is it the expected behavior ?

@aymeric-giraudet Works perfectly for my use case and just pushed it to production šŸ˜ƒ

@LefanTan : Yes itā€™s a known limitation of getServerState, since itā€™s not rendered by Next.js it does not have access to its router so useRouter returns null. Iā€™ll make note of it for us to add disclaimers but to circumvent it you should maybe avoid using useRouter directly and you could maybe wrap it into a custom hook that returns a stub object when useRouter returns null.

@wenche : Thanks for the reproduction ! I see you have reactStrictMode: true, these bugs indeed do happen on strict mode while in development mode. The component mounts twice, and it seems like the router middleware does not get unsubscribed on first unmount, so the router tries to setUiState on the first instance which is stale. I will look for a way for InstantSearch to properly recreate the router.

Unfortunately, we see some more issues when we use this new library. We have created an example repository to highlight our findings (Let me know if you need our Algolia app id and the api key ).

Disclaimer: Some of these issues might have the same root cause, but itā€™s not easy to find a way to explain without too much confusion. Thatā€™s why I tried to split it up šŸ˜…

Steps to reproduce issue no1

  1. On the front page, we have a client side link with an initial query param added. ?Data_Set%5Bquery%5D=data Click this link named Take me to the search!
  2. Server side everything seems to be OK, but then some client side code kicks in, removes the initial query param and pushes this to the router state so that the search state is changed to the empty search
  3. The correct state is pushed to the router history, so on browser back everything works as expected

Steps to reproduce issue no2

  1. Go to the search page with an empty search
  2. Hard refresh the page (cmd+r)
  3. Choose a filter/type something in the search box
  4. Some client side code will reset the search state, and we see The UI state for the index "Data_Set" is not consistent with the widgets mounted. warning in the console

Steps to reproduce issue no3

  1. Go to the search page
  2. Search for something, like data
  3. Go to the detail page for a hit
  4. Go back to the search page using browser back
  5. Server side everything is fine, but then some client side code will reset the search state

Plot twist If we use the onStateChange function, issue no1 and no3 seems to work.

These findings are in addition to the one already mentioned above with the clean up issue šŸ‘

Thank you for fixing the issue, but getServerState doesnā€™t seem to know how to handle router during SSR like NextJS does?

This is the errors Iā€™m getting on everywhere that uses next/router:

Cannot read property 'query' of null

@avremel

Even if you donā€™t put a <title> tag in <Head> it does override with an empty value when routerā€™s push is called šŸ˜•

For now I would suggest either removing Head completely if you can, otherwise with useRef you can store the created title in windowTitle and use it as a child for <Head><title>...</title></Head>

Iā€™ll look for other solutions for the definitive package šŸ˜ƒ

@aymeric-giraudet Thank you for the package! Very helpful to inspect the route as a drop in replacement for uiState which IME is unreliable (see here).

Iā€™ve noticed that windowTitle doesnā€™t seem to work. It flashes the new title for a second and then reverts back to the old one.

Iā€™ve provided custom routing functions:

 routing: {
      router: createInstantSearchNextRouter({
        serverUrl,
        routerOptions: {
          windowTitle: (routeState) => windowTitle(routeState, facetConfig),
          createURL,
          parseURL,
        },
      }),
      stateMapping: {
        routeToState: (params) => routeToState(params, facetConfig),
        stateToRoute,
      },
    }

Hi @aymeric-giraudet work perfectly, thanks a lot for your work.

When do you think the final package will be published?

Hi @Raesta,

Sorry for the double post but it should be fixed by now, with version 0.0.5 What I did is I added this line to package.json in instantsearch-router-next-experimental :

"exports": {
  "webpack": "./dist/index.cjs"
}

To instruct webpack to use cjs even if mjs is available, and it seems to fix things.

You can let me know if this fixes things on your end too, thanks ! šŸ˜ƒ

I also needed this and figured out how to do this until thereā€™s official support. Itā€™s a bit hacky but it works for my use case.

Hereā€™s the router code (I suggest you put it in a .js file):

import { history as InstantSearchHistory } from 'instantsearch.js/es/lib/routers';
import router from 'next/router';

export function history(url) {
  const newHistory = InstantSearchHistory({
    getLocation() {
      if (typeof window === 'undefined') {
        return new URL(url);
      }

      return window.location;
    },
  });

  newHistory.write = function (routeState) {
    if (typeof window === 'undefined') {
      return;
    }

    const url = this.createURL(routeState);
    const title = this.windowTitle && this.windowTitle(routeState);
    if (this.writeTimer) {
      clearTimeout(this.writeTimer);
    }
    this.writeTimer = setTimeout(() => {
      if (title) {
        window.document.title = title;
      }
      const shouldWrite = this.shouldWrite(url);
      if (shouldWrite) {
        router.push(url, url);
        this.pushed = true;
        this.latestAcknowledgedHistory = window.history.length;
      }
      this.inPopState = false;
      this.externalChange = false;
      this.writeTimer = undefined;
    }, this.writeDelay);
  }.bind(newHistory);

  newHistory.onUpdate = function (callback) {
    if (typeof window === 'undefined') {
      return;
    }

    this._onPopState = (url) => {
      if (this.writeTimer) {
        clearTimeout(this.writeTimer);
        this.writeTimer = undefined;
      }

      this.inPopState = true;

      const newRoute = this.read();
      callback(newRoute);
    };

    this._onRouteChangeComplete = (url) => {
      if (this.writeTimer) {
        clearTimeout(this.writeTimer);
        this.writeTimer = undefined;
      }

      if (this.pushed) {
        this.pushed = false;
        return;
      }

      this.externalChange = true;

      const newRoute = this.read();
      callback(newRoute);
    };

    router.events.on('routeChangeComplete', this._onRouteChangeComplete);
    router.beforePopState((event) => this._onPopState(event.url));
  }.bind(newHistory);

  newHistory.dispose = function () {
    this.isDisposed = true;

    if (this.writeTimer) {
      clearTimeout(this.writeTimer);
    }

    this.write({});
  }.bind(newHistory);

  newHistory.shouldWrite = function (url) {
    if (typeof window === 'undefined') {
      return false;
    }

    const lastPushWasByISAfterDispose = !(this.isDisposed && this.latestAcknowledgedHistory !== window.history.length);

    return !this.inPopState && !this.externalChange && lastPushWasByISAfterDispose && url !== window.location.href;
  }.bind(newHistory);

  return newHistory;
}

Use it like so:

import { history } from '<where you put the router file>';
import { InstantSearch } from 'react-instantsearch-hooks-web';
import { useMemo } from 'react';

export const Component = ({
  url,
  // ...
}) => {
  // ...

  return (
    <InstantSearch
      {/* ... */}
      routing={useMemo(
        () =>
          url
            ? {
                router: history(url),
              }
            : false,
        [url]
      )}
    >
      {/* ... */}
    </InstantSearch>
  );
};

The url comes from getServerSideProps and is used for SSR like so:

import { Component } from '<where you put your component>';
import { getServerState } from 'react-instantsearch-hooks-server';
import { InstantSearchSSRProvider } from 'react-instantsearch-hooks-web';

const Page = ({ url, serverState, /* ... */ }) => {
  // ...

  return (
    <InstantSearchSSRProvider {...serverState}>
      <Component url={url}>
        {/* ... */}
      </Component>
    </InstantSearchSSRProvider>
  );
};

export async function getServerSideProps(ctx) {
  // maybe there's a better way to get the url but this works
  const { req } = ctx;
  const protocol = req.headers.referer?.split('://')[0] || 'https';
  const serverUrl = `${protocol}://${req.headers.host}${req.url}`;
  const url = serverUrl;

  const serverState = await getServerState(<App Component={Page} pageProps={{ url, /* ... */ }} />);

  return { props: { url, serverState, /* ... */ } };
}

Hi, hereā€™s a sandbox with another workaround that shares some similarities with @JulesVerdijk : https://codesandbox.io/s/rish-next-router-handler-is22ev?file=/pages/[category].tsx.

It consists of a useNextRouterHandler() Hook that returns:

  • an initialUiState that needs to be passed to <InstantSearch> (for SSR)
  • a <NextRouterHandler /> empty component that should be mounted in <InstantSearch> (to be able to use Hooks from React InstantSearch Hooks)

It required an explicit definition of the route/state mapping, and supports Next.js dynamic routes (by specifying the route query to inject).

I tested it on some sandbox shared in the issues, and it works well. Feel free to share below if there are edge cases that are not handled by this workaround.

I ran into this problem as well and created a, pretty hacky, fix that seems to work (not used in production yet, so fingers crossed). What I did was taking full control of the routing, not using the router provided by Instantsearch.

Requirement for this solution is a helper that can convert an UiState to a URL (like the createURL function in the algolia router) and back again (like the parseURL function).

In basis the solution works like this:

const AlgoliaCollection: React.FC<any> = ({ serverState }) => {
    const nextRouter = useRouter();

    const onStateChange: InstantSearchProps['onStateChange'] = ({ uiState }) => {
        setUiStateFromAlgolia(uiState);
        const urlForUiState = uiStateToUrl(uiState, INDEX_NAME);

        nextRouter.push(urlForUiState, undefined, { shallow: true });

    return (
        <InstantSearchSSRProvider {...serverState}>
            <InstantSearch searchClient={algoliaClient} indexName={INDEX_NAME} onStateChange={onStateChange}>
                <RefinementList />
                <Hits />
            </InstantSearch>
        </InstantSearchSSRProvider>
    );
}

export default AlgoliaCollection;

So you now control the updating of the UiState yourself, and use this to update the NextRouter accordingly. However this is not enough, it works one way only. On onStateChange the route changes, but if you route through Next (e.g. press the back button) the UiState does not update accordingly. To be able to do this you need to be able to setUiState on a routing event, but InstantSearch does not propegate a setUiState function outside of the onStateChange prop (can we please get that Algolia?). However, this function Ć­s available in child-components in the useInstantSearch hook. I abused this to set the UiState based on routing changes, something like this:

const UpdateStateBasedOnRoute: React.FC<{ uiStateFromAlgolia: UiState, indexName: string }> = ({ uiStateFromAlgolia, indexName }) => {
    const { setUiState } = useInstantSearch();
    const nextRouter = useRouter();

    // This fires on every uiStateFromAlgoliaChange
    useEffect(() => {
        // Create a URL based on the uiStateFromAlgolia (which is set in `AlgoliaCollection` based on the onStateChange event
        const urlFromUiState = uiStateToUrl(uiStateFromAlgolia, indexName);

        // Create a URL based on the actual route
        const urlFromRouter = nextRouter.asPath.includes('?') ? `?${nextRouter.asPath.split('?')[1]}` : ``;

        // These URLs should be identical, if not, routing has probably changed and thus the UiState should be updated accordingly
        //
        if (urlFromUiState !== urlFromRouter) {
            const newUiState = urlToUiState(`${process.env.NEXT_PUBLIC_WEB_URL}${nextRouter.asPath}`, indexName);
            setUiState(newUiState);
        } else {
            setUiState(uiStateFromAlgolia);
        }
    }, [uiStateFromAlgolia, nextRouter]);

    return null;
};

// uiStateFromServer is a URL to UiState done on the server, so the `uiStateFromAlgolia` is correct on first load
const AlgoliaCollection: React.FC<any> = ({ serverState, uiStateFromServer }) => {
    const nextRouter = useRouter();

    const [uiStateFromAlgolia, setUiStateFromAlgolia] = useState(uiStateFromServer);

    const onStateChange: InstantSearchProps['onStateChange'] = ({ uiState }) => {
        setUiStateFromAlgolia(uiState);
        const urlForUiState = uiStateToUrl(uiState, INDEX_NAME);

        nextRouter.push(urlForUiState, undefined, { shallow: true }).then(() => {});
    };

    return (
        <InstantSearchSSRProvider {...serverState}>
            <InstantSearch searchClient={algoliaClient} indexName={INDEX_NAME} onStateChange={onStateChange}>
                <UpdateStateBasedOnRoute uiStateFromAlgolia={uiStateFromAlgolia} indexName={INDEX_NAME} />
                <RefinementList />
                <Hits />
            </InstantSearch>
        </InstantSearchSSRProvider>
    );
}

export default AlgoliaCollection;

Note that I pass uiStateFromAlgolia as a prop to UpdateStateBasedOnRoute , but it is also available from useInstantSearch however I found passing it made it update more smoothly (it is a hacky solution!). With again a disclaimer that this works in a development environment but I havenā€™t used it in production yet.

This would work a lot more smoothly if we could actually get a setUiState prop directly on InstantSearch so this can all be controlled within the main component instead of in an empty child component.

@dhayab posted a workaround using Next useRouter and useEffect in a discussion. Worked for me šŸ˜ƒ

https://github.com/algolia/react-instantsearch/discussions/3376#discussioncomment-2297218

You can provide a custom router, you donā€™t need to use history, the methods are described here https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/#widget-param-routing

Iā€™d advise looking at the implementation of the history router, because thereā€™s some edge cases we already cover there as a base

Iā€™m running into issue no. 2 here as well, the broken back button. I think itā€™s caused by the instantsearch.js history implementation performing itā€™s own history mutations, over here:

https://github.com/algolia/instantsearch.js/blob/0a517609de103eef4f8edfefe6e28a1d79a14209/src/lib/routers/history.ts#L142

e.g., try the following on any app rendered by next.js (in this example Iā€™ll use https://nextjs.org/)

  1. visit https://nextjs.org/
  2. open dev tools console
  3. run command history.pushState({}, '', '/test-foo-bar') and see the url get updated to the new pathname. (this mimics an updated url due to filtering)
  4. click the nav item ā€œShowcaseā€ (this mimics clicking a search result)
  5. hit the back button
  6. the pathname goes from /showcase to /test-foo-bar, in the UI nothing happens, youā€™ll stay on the ā€œshowcaseā€ page.

~The algolia hook history API needs something like custom routing method in order to fix this.~

~e.g. calling context:~

import { InstantSearch } from "react-instantsearch-hooks-web";
import { history } from 'instantsearch.js/es/lib/routers';
import { useRouter } from "next/router";

const router = useRouter();

<InstantSearch
  routing={{
    router: history({
      // please note that `onUpdate` is not an existing part of the api
      onUpdate: (url) => router.push(url),
    })
  }}
/>

~When an onUpdate is provided the history instance should obviously no more do itā€™s own history mutations.~

~This also alllows us to perform a history.replace() instead of history.push() which makes more sense in some filtering contexts.~

Edit: the above is already possible with a custom history handler as suggested below.

Hey @dsbrianwebster ā€“ this is likely because Next.js is using Strict Mode, which isnā€™t yet supported in the library with React 18.

Our <InstantSearch> component reads the URL query params to set the initial state, and write the browser URL on unmount to reset it. This is a first-class behavior of the underlying InstantSearch.js library. The problem is that during the second Strict Mode render, the URL has been reset, and therefore the second render is unable to recover the initial state. This breaks URL synchronization.

Weā€™re working on React 18 Strict Mode support this week so you can expect a fix in the coming days.

In the mean time, could you disable Strict Mode on the <InstantSearch> component?

Edit: Strict Mode support was introduced in v6.28.0.