Suspense > prefetchQuery (server) > useQuery : how cache here could work when navigating
Unanswered
Scaled Quail posted this in #help-forum
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
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
CategoryPostsClient.tsx
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
}