typesense / typesense-instantsearch-adapter

A JS adapter library to build rich search interfaces with Typesense and InstantSearch.js

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

react-instantsearch-router-nextjs breaks routing

stevebutler2210 opened this issue · comments

Description

When using Typesense with Next JS and React Instantsearch, attempting to use the react-instantsearch-router-nextjs package to create a router for the InstantSearch routing prop causes the application to break (when searching, filtering etc, the params briefly appear in the URL but are then cleared down, resetting the search to the default results).

Without this setup, I am getting the following warning in the console

[InstantSearch] You are using Next.js with InstantSearch without the "react-instantsearch-router-nextjs" package.
This package is recommended to make the routing work correctly with Next.js.
Please check its usage instructions: https://github.com/algolia/instantsearch/tree/master/packages/react-instantsearch-router-nextjs

What is more problematic is that this package / setup is required to achieve SSR with InstantSearch and Next JS (see here).

Is there any other way to achieve SSR with Next / Typesense, without using the react-instantsearch-router-nextjs routing?

Steps to reproduce

Example of a Next JS page component where the described issue can be observed. I am using the sample books data / schema etc from the Typesense Building A Search Application docs

import singletonRouter from "next/router";
import {
  InstantSearch,
  SearchBox,
  Hits,
  Pagination,
  RefinementList,
  SortBy,
} from "react-instantsearch";
import { createInstantSearchRouterNext } from "react-instantsearch-router-nextjs";
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";

const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
  server: {
    apiKey: "YOUR_API_KEY", // Be sure to use the search-only-api-key
    nodes: [
      {
        host: "YOUR_CLUSTER.typesense.net",
        port: 443,
        protocol: "https",
      },
    ],
  },
  // The following parameters are directly passed to Typesense's search API endpoint.
  //  So you can pass any parameters supported by the search endpoint below.
  //  queryBy is required.
  additionalSearchParameters: {
    query_by: "title,authors",
  },
});
const searchClient = typesenseInstantsearchAdapter.searchClient;

const transformItems = (items) => {
  return items.map((item) => ({
    ...item,
    count: `(${item.count})`,
  }));
};

const SearchPage = () => {
  return (
    <InstantSearch
      searchClient={searchClient}
      indexName="books"
      routing={{
        router: createInstantSearchRouterNext({
          singletonRouter,
        }),
      }}
      future={{
        preserveSharedStateOnUnmount: true,
      }}
    >
      <div
        style={{
          display: "flex",
          flexDirection: "row",
          gap: "50px",
          fontSize: "24px",
        }}
      >
        <div style={{ width: "50%" }}>
          <SearchBox />
          <Hits
            hitComponent={({ hit }) => (
              <div>
                <h6>{hit.title}</h6>
                <p>{hit.authors.join(", ")}</p>
                <p>{hit.publication_year}</p>
              </div>
            )}
          />
          <Pagination />
        </div>
        <div style={{ display: "flex", flexDirection: "column" }}>
          <p>Publication Years</p>
          <RefinementList
            attribute="publication_year"
            transformItems={transformItems}
            sortBy={["name"]}
          />
        </div>
        <SortBy
          items={[
            { label: "Relevance", value: "books" },
            { label: "Title", value: "books/sort/title:asc" },
            {
              label: "Publication Year",
              value: "books/sort/publication_year:desc",
            },
          ]}
          defaultValue={"title"}
        />
      </div>
    </InstantSearch>
  );
};

export default SearchPage;

Expected Behavior

The params from the search / filters / sorting etc should be persisted correctly, without being cleared down.

Actual Behavior

The route updates, clearing all params, shortly after taking any action that affects it

Screen.Recording.2024-04-16.at.12.14.17.mov

Metadata

Typesense Version: 1.8.2

OS: MacOS 14.4.1

Next JS Version: 14.1.0

react-instantsearch Version: 7.7.1

react-instantsearch-router-nextjs Version: 7.7.1

The typesense-instantsearch-adapter is a data transformation library that Instantsearch calls with a set of search parameters, which the adapter translates to the Typesense API format, and then translates the API response from Typesense to a format the widgets understand.

So any UI / routing behaviors are almost always outside the control of the typesense adapter itself.

I would recommend checking if you're able to replicate the issue with an Algolia index, and if so, report the issue in the Algolia Instantsearch repo directly.

If it works properly with an Algolia index, and the issue only exists in Typesense, could you share GitHub links to both the Algolia and Typesense versions, and I can take a closer look to see if it's a data transformation issue that's causing any UI issues as a side-effect.

Is this with app router or pages? Because react-instantsearch-router-nextjs and InstantSearch is for the pages directory. For the app router you have to use InstantSearchNext from react-instantsearch-nextjs. Docs

Thanks @thedevdavid, yeah this is using the pages router 👍 @jasonbosco I'll try that out and get back to you when I've had a chance to take a better look. I can see the showcase demo is configured for SSR, so that at least tells me what I'm doing should be possible—my gut feel is I've made a silly mistake somewhere and I'm just not seeing it at the moment. Will report back

Ultimately, the issue was caused by something completely different—skimming Algolia issues I came across this, which sounded awfully like the behaviour I was seeing.

After a lot of poking around, I ended up looking at our _app.tsx page, which looked like:

import type { AppProps } from "next/app";
import { useRouter } from "next/router";
import { ApolloProvider } from "@apollo/client";
import { AnimatePresence } from "framer-motion";
import { appWithTranslation } from "next-i18next";
import { graphqlClient } from "@/lib/graphqlClient";
import "@/styles/globals.scss";
import "@/styles/theme.scss";

const App = ({ Component, pageProps }: AppProps) => {
  const client = graphqlClient();
  const router = useRouter();

  return (
    <ApolloProvider client={client}>
      <AnimatePresence
        mode="wait"
        initial={false}
        onExitComplete={() => window.scrollTo(0, 0)}
      >
        <Component {...pageProps} key={router.asPath} />
      </AnimatePresence>
    </ApolloProvider>
  );
};

export default appWithTranslation(App);

Simply removing the key prop from Component completely fixes the issues I was experiencing!

I'm going to dig into the exact cause here a little deeper, but I'll close this issue now as it was clearly unrelated to Typesense. Apologies for clogging up the inbox!