algolia / instantsearch

⚡️ Libraries for building performant and instant search and recommend experiences with Algolia. Compatible with JavaScript, TypeScript, React and Vue.
https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/js/
MIT License
3.73k stars 525 forks source link

Hydration Issues with NextJS 14 App Router #6307

Closed KallendJack closed 3 months ago

KallendJack commented 3 months ago

🐛 Current behavior

I have been following the documentation for react-instantsearch-next here

When using this and going to a search page via a hard refresh or directly to the URL. There are hydration errors, which end up displaying the wrong data from Algolia. At first, I thought this may be my setup, but I still receive these errors when setting it up exactly like the docs. This is my current component:

'use client'

import algoliasearch from 'algoliasearch/lite'
import { Configure } from 'react-instantsearch'
import { InstantSearchNext } from 'react-instantsearch-nextjs'
import { useMediaQuery } from '@mantine/hooks'

import type { Category, CategoryFacets } from '@/types'
import { algoliaConfig } from '@/config'
import {
    AlgoliaHitsHeading,
    AlgoliaFacetDrawer,
    AlgoliaCurrentRefinementsHorizontal,
    AlgoliaSortBy,
    AlgoliaHits,
    AlgoliaPagination,
} from '@/components/Algolia'

const searchClient = algoliasearch(algoliaConfig.applicationId, algoliaConfig.searchApiKey)

type AlgoliaListPageWithDrawerProps = {
    isLoggedIn: boolean
    category?: Category
    query?: string
    filters: string
    facets: CategoryFacets
}

export function AlgoliaListPageWithDrawer(props: AlgoliaListPageWithDrawerProps) {
    const { isLoggedIn = true, category, query = '', filters, facets } = props

    const isMobile = useMediaQuery('(max-width: 767px)')

    return (
        <InstantSearchNext
            searchClient={searchClient}
            indexName={algoliaConfig.indexName}
            routing={algoliaConfig.routing}
            insights={algoliaConfig.insights}
            future={{ preserveSharedStateOnUnmount: true }}
        >
            <div data-testid="algolia-list-page-with-drawer" className="container">
                <Configure hitsPerPage={16} query={query} filters={filters} />

                <AlgoliaHitsHeading category={category} query={query} />

                <AlgoliaCurrentRefinementsHorizontal className="mb-[15px] lg:hidden" />

                <div className="mb-[15px] flex items-center justify-between gap-x-[15px]">
                    <div className="flex w-full items-center gap-x-[15px]">
                        <div className="w-full lg:min-w-[160px] lg:max-w-[160px]">
                            <AlgoliaFacetDrawer facets={facets} />
                        </div>

                        <AlgoliaCurrentRefinementsHorizontal className="hidden lg:flex" />
                    </div>

                    <div className="w-full lg:max-w-[270px]">
                        <AlgoliaSortBy
                            items={[
                                {
                                    label: isMobile ? 'Default' : 'Sort By: Default',
                                    value: algoliaConfig.indexName,
                                },
                                {
                                    label: isMobile
                                        ? 'Price - low to high'
                                        : 'Sort By: Price - low to high',
                                    value: algoliaConfig.indexNamePriceAscending,
                                },
                                {
                                    label: isMobile
                                        ? 'Price - high to low'
                                        : 'Sort By: Price - high to low',
                                    value: algoliaConfig.indexNamePriceDescending,
                                },
                            ]}
                        />
                    </div>
                </div>

                <div className="flex flex-col gap-y-[30px]">
                    <AlgoliaHits isLoggedIn={isLoggedIn} />
                    <AlgoliaPagination />
                </div>
            </div>
        </InstantSearchNext>
    )
}

And the page rendering it (page.tsx)

import type { Metadata } from 'next'
import { notFound } from 'next/navigation'

import { featureConfig, metaConfig } from '@/config'
import { getSession } from '@/data/auth'
import { AlgoliaListPageWithDrawer } from '@/components/Algolia'

export const dynamic = 'force-dynamic'

export const metadata: Metadata = {
    title: `Search | ${metaConfig.baseMetaTitle}`,
    description: 'Here are the results for your search query.',
}

type SearchProps = {
    searchParams: { q: string }
}

export default async function Search(props: SearchProps) {
    const { searchParams } = props

    if (!featureConfig.search.enabled) {
        return notFound()
    }

    const session = await getSession()
    const isLoggedIn = session?.guest_user === false
    const query = searchParams.q
    const filters = 'NOT availability:"withdrawn"'
    const facets = [
        { label: 'Brand', code: 'brand' },
        { label: 'Product Type', code: 'product_type' },
    ]

    return (
        <AlgoliaListPageWithDrawer
            isLoggedIn={isLoggedIn}
            query={query}
            filters={filters}
            facets={facets}
        />
    )
}

This is making the whole package unusable with SSR for my project, as it is displaying incorrect results and crashing with hydration errors. Any assistance would be greatly appreciated.

package.json

"next": "^14.2.5",
"react": "^18.3.1",
"react-instantsearch": "^7.11.4",
"react-instantsearch-nextjs": "^0.3.7",

🔍 Steps to reproduce

  1. Visit the search page
  2. Hard refresh
  3. See lots of hydration errors

Live reproduction

N/A

💭 Expected behavior

There are no hydration errors and the correct data is displaying.

Package version

"react-instantsearch": "^7.11.4","react-instantsearch-nextjs": "^0.3.7",

Operating system

macOS

Browser

Chrome

Code of Conduct

aymeric-giraudet commented 3 months ago

Hi @KallendJack,

Can't reproduce without a link, but where exactly does the hydration problem occur ? I think later versions of React now help with visualization, but you can compare the DOM markup between the initial HTTP request and what's rendered after hydration, this would help pinpoint where the problem lies exactly.

It could be due to other libraries you're using like Mantine which may render differently on the server (if isMobile is true on the server and false on the client, you'd have an error like this). Without a reproduction and exact pinpointing of the DOM diffs between server and client it's hard to tell.

KallendJack commented 3 months ago

Hi @KallendJack,

Can't reproduce without a link, but where exactly does the hydration problem occur ? I think later versions of React now help with visualization, but you can compare the DOM markup between the initial HTTP request and what's rendered after hydration, this would help pinpoint where the problem lies exactly.

It could be due to other libraries you're using like Mantine which may render differently on the server (if isMobile is true on the server and false on the client, you'd have an error like this). Without a reproduction and exact pinpointing of the DOM diffs between server and client it's hard to tell.

Hi @aymeric-giraudet. Thanks for your response. Here is the link to the two pages I am seeing the issues:

https://feature-app-dir.peracto3carbon.pub/category/simple-products https://feature-app-dir.peracto3carbon.pub/search?q=s

I have also tried it without Mantine, but I have tried removing many things with no luck.

Locally, it comes up saying the hit count doesn't match:

image

However, this component only contains the following:

'use client'

import { useStats } from 'react-instantsearch'

export function AlgoliaHitCount() {
    const { nbHits } = useStats()

    return (
        <span data-testid="algolia-hit-count" className="text-sm font-normal text-brand-black">
            ({nbHits})
        </span>
    )
}
aymeric-giraudet commented 3 months ago

Hi @KallendJack,

This is due to having two different InstantSearchNext components on the page which is not supported.

I can think of two ways to fix this, in order of simplicity/relevance :

KallendJack commented 3 months ago

Hi @KallendJack,

This is due to having two different InstantSearchNext components on the page which is not supported.

I can think of two ways to fix this, in order of simplicity/relevance :

  • Can you use InstantSearch instead of InstantSearchNext for the autocomplete ? I'm not sure this needs to be rendered on the server and is even relevant for SEO. This could even be a browser-only component. I see it loads and injects hits on the page on the server even though it's not used in markup. This would make the page perform better too.
  • Haven't tried it, but having a single InstantSearchNext around the root may work ?

Hi @aymeric-giraudet, thanks for this I'll give it a go

KallendJack commented 3 months ago

@aymeric-giraudet This fixes it... kind of 😆. I am now using InstantSearch for the autocomplete and InstantSearchNext for the category page. However, when the Autocomplete loads in it seems to overwrite the results on the category page. It also seems to use the component from the Autocomplete, which would explain why it kept only showing 3 results in the previous issue I had. Is there a way to have both a InstantSearch and InstantSearchNext on the same page without this problem?

Thanks again.

UPDATE

I realized that I had my Autocomplete within the category page as part of my no-results view. This caused the <Configure to be nested inside, causing the error. Changing this to a regular input has fixed the issue. Thanks a lot for your help. I will mark this issue as closed.

Haroenv commented 3 months ago

Is it an option to use

InstantSearch is designed to be used only once per page, so it's also designed to have only one set of server side rendering results for example

KallendJack commented 3 months ago

This is now resolved for me. Thanks for all the help. Please see comments above to see how I resolved.