Next.js Discord

Discord Forum

Suspense > prefetchQuery (server) > useQuery : how cache here could work when navigating

Unanswered
Scaled Quail posted this in #help-forum
Open in Discord
Scaled QuailOP
I have a page with multiple posts and everything is working, but I'm noticing a problem when navigating between pages.

The goal here is to have a proper rendering for SEO (using suspense and streaming).
When I see the source code, everything is fine and I have my data on the page. Lazy loading (loading suspense) works fine.

But now when I load this page for the first time, go to another page and come back to this page, the previously loaded data is not displayed and I see the loading suspense again.
I would like to display posts when they are loaded for the first time (without impacting suspense/streaming for SEO), while useQuery is refetching data...
How can I do this?

page.tsx
import CategoryPostsServer from '@/_components/posts/CategoryPostsServer'
import { Suspense } from 'react'

export default async function CategorySlugPlural({
  params,
}: {
  params: Promise<{ category_slug_plural: string; locale: string }>
}) {
  const _params = await params

  return (
    <>
      <div>
        <Suspense fallback={<div>Loading...</div>}>
          <CategoryPostsServer
            category_slug_plural={_params.category_slug_plural}
            locale={_params.locale}
          />
        </Suspense>
      </div>
    </>
  )
}

6 Replies

Scaled QuailOP
CategoryPostsServer.tsx
import getPosts from '@/_api/post/getPosts'
import CategoryPostsClient from '@/_components/posts/CategoryPostsClient'
import getQueryClient from '@/_helpers/getQueryClient'
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'

export default async function CategoryPostsServer({
  category_slug_plural,
  locale,
}: {
  category_slug_plural: string
  locale: string
}) {
  const queryClient = getQueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['getPosts', category_slug_plural, locale],
    queryFn: () =>
      getPosts({
        category_slug_plural: category_slug_plural,
      }),
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <CategoryPostsClient
        category_slug_plural={category_slug_plural}
        locale={locale}
      />
    </HydrationBoundary>
  )
}
CategoryPostsClient.tsx
'use client'
import getPosts from '@/_api/post/getPosts'
import { useQuery } from '@tanstack/react-query'

export default function CategoryPostsClient({
  category_slug_plural,
  locale,
}: {
  category_slug_plural: string
  locale: string
}) {
  const { data } = useQuery({
    queryKey: ['getPosts', category_slug_plural, locale],
    queryFn: () =>
      getPosts({
        category_slug_plural,
      }),
    staleTime: 1000 * 60 * 5, // 5 min
    // refetchOnWindowFocus: true,
    // refetchInterval: 1000,
    // initialData: posts,
  })

  return (
    <div>
      <div>
        {data &&
          data.post_types.map((post_type) => (
            <h1 key={post_type.id} className="text-4xl">
              {post_type.title_plural}
            </h1>
          ))}
      </div>
    </div>
  )
}
Scaled QuailOP
# 2nd approch
I tried another approch by using useSuspenseQuery but now I have another issue using my custom fetch API function
Error: Server Functions cannot be called during initial render. This would create a fetch waterfall. Try to use a Server Component to pass data to Client Components instead.

page.tsx
`tsx
import { Suspense } from 'react'
import CategoryPostsClient from '@/_components/posts/CategoryPostsClient'
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'

export default async function CategorySlugPlural({
  params,
}: {
  params: Promise<{ category_slug_plural: string; locale: string }>
}) {
  const _params = await params

  return (
    <>
      <div>
        <Suspense fallback={<div>Chargement...</div>}>
          <ReactQueryStreamedHydration>
            <CategoryPostsClient
              category_slug_plural={_params.category_slug_plural}
              locale={_params.locale}
            />
          </ReactQueryStreamedHydration>
        </Suspense>
      </div>
    </>
  )
}


CategoryPostsClient.tsx

'use client'
import getPosts from '@/_api/post/getPosts'
import { categoryPostsResponseType } from '@/_ts/types/responses/categoryPostsResponseType'
import { useSuspenseQuery } from '@tanstack/react-query'

export default function CategoryPostsClient({
  category_slug_plural,
  locale,
}: {
  category_slug_plural: string
  locale: string
}) {
  const { data } = useSuspenseQuery<categoryPostsResponseType>({
    queryKey: ['getPosts', category_slug_plural, locale],
    queryFn: () => {
      return getPosts({ category_slug_plural })
    },
    staleTime: 1000 * 60 * 5, // 5 min
  })

  return (
    <div>
      <div>
        {data &&
          data.post_types.map((post_type) => (
            <h1 key={post_type.id} className="text-4xl">
              {post_type.title_plural}
            </h1>
          ))}
      </div>
    </div>
  )
}
getPosts
'use server'

import { ApiRoutes } from '@/_api/apiRoutes'
import { buildApiRoute } from '@/_api/buildApiRoute'
import fetchApi from '@/_helpers/fetchApi'
import { categoryPostsResponseType } from '@/_ts/types/responses/categoryPostsResponseType'

export default async function getPosts({
  category_slug_plural,
}: {
  category_slug_plural: string
}): Promise<categoryPostsResponseType> {
  const { url, method } = buildApiRoute(ApiRoutes.posts)

  return fetchApi({
    url: url,
    method: method,
    params: {
      category_slug_plural,
    },
    // cache: 'no-cache',
  })
}
fetchApi
import getClientSideCookies from '@/_helpers/getClientSideCookies'
import { HttpMethod } from '@/_ts/types/api-route-type'
import { getLocale } from 'next-intl/server'
import getCookies from '@/getCookies'

// https://trackjs.com/blog/common-errors-in-nextjs-caching/
// https://nextjs.org/docs/app/building-your-application/caching
export default async function fetchApi<T = void>({
  url,
  method,
  params,
  cache,
}: {
  url: string
  method: HttpMethod
  params?: Record<string, string>
  cache?: RequestCache
}): Promise<T> {
  console.log('fetchApi (' + url + ')')
  const headers: Record<string, string> = {
    Accept: 'application/json',
  }

  // sleep 2s
  // await new Promise((resolve) => setTimeout(resolve, 2000))

  
// Server Side
  if (typeof window === 'undefined') {
    const cookieStore = await getCookies()

    headers['Cookie'] = cookieStore.toString()
    const xsrfToken = cookieStore.get('XSRF-TOKEN')
    if (xsrfToken)
      headers['X-XSRF-TOKEN'] = cookieStore.get('XSRF-TOKEN')!.value

    headers['referer'] = process.env.APP_URL as string
    headers['Accept-Language'] = await getLocale()
  } else {
    // Client side
    headers['Accept-Language'] =
      (getClientSideCookies({ name: 'NEXT-LOCALE' }) as string) ||
      window.location.pathname.split('/')[1]

    const xsrfToken = getClientSideCookies({ name: 'XSRF-TOKEN' })
    if (xsrfToken) headers['X-XSRF-TOKEN'] = xsrfToken as string
  }

  // Todo : gérer les sessions expirées (ban ...)

  const response = await fetch(
    url + (method === 'GET' ? '?' + new URLSearchParams(params) : ''),
    {
      method,
      credentials: 'include',
      headers,
      // cache: 'force-cache',
      cache,
      body: method !== 'GET' ? JSON.stringify(params) : undefined,
    },
  )

  if (!response.ok) {
    const errorResponse = await response.json()
    throw new Error(
      errorResponse.message || `HTTP error! status: ${response.status}`,
    )
  }

const contentType = response.headers.get('content-type')
  if (contentType && contentType === 'application/json') {
    // Retourne le contenu JSON si le type générique existe, sinon void
    return (await response.json()) as T
  }

  return undefined as T
}