1. 리액트 쿼리 Hydration (v5)
*참고: https://tanstack.com/query/latest/docs/framework/react/reference/hydration
- 'dehydrate'
- 서버 컴포넌트에서 fetch한 데이터를 클라이언트 컴포넌트에 전달하려면 serialization을 거쳐야 한다.
- serialization이란 데이터를 저장 혹은 전송 가능한 포맷으로 변환하는 과정을 말한다.
- 리액트 쿼리의 dehydrate는 이 serialization과 유사한 개념으로, 리액트 쿼리가 server-side에서 생성된 쿼리를 client-side로 전달하기 위해 고정된(frozen) 형태로 만드는 과정을 말한다.
- dehydrate된 쿼리는 local storage나 다른 영구적인 위치에 저장하는 데에도 유용하다.
- 기본적으로 리액트 쿼리는 성공한 쿼리만 dehydrate하기 때문에 Error나 undefined 같은 JSON으로 직렬화 할 수 없는 값들은 포함하지 않는다. 이들은 직접 직렬화해야 한다.
- Date, Map, Set, BigInt, Infinity, NaN, -0, 정규 표현식 등도 직렬화를 지원하지 않음. (https://tanstack.com/query/latest/docs/framework/react/guides/ssr#serialization)
// server-side
import { QueryClient, dehydrate } from '@tanstack/react-query'
const queryClient = new QueryClient();
const dehydratedState = dehydrate(queryClient);
// queryClient를 dehydrate 한다는 점에 주의할 것
// dehydratedState은 직렬화된 형식이 아님에 주의할 것
- 'hydrate'
- dehydrate의 반대 과정으로, 리액트 쿼리가 dehydrate된 state를 client-side에서 캐싱한다.
// client-side
import { QueryClient, hydrate } from '@tanstack/react-query'
const queryClient = new QueryClient();
hydrate(queryClient, dehydratedState);
// client-side에서 생성한 새 queryClient에 server-side에서 dehydrate한 queryClient를 추가한다.
// 만약 dehydrate된 쿼리 중에 이미 클라이언트 queryCache에 존재하는 쿼리가 있다면 해당 쿼리의 hydration은
// 덮어쓰지 않고 삭제된다.
- 'HydrationBoundary'
- useQueryClient()를 호출하여 가장 가까운 queryClient context의 queryClient에 dehydratedState를 추가한다.
- 'hydrate'와 다르게 client에 이미 data가 존재하면, update timestamp를 기반으로 새 쿼리를 병합한다. (https://tanstack.com/query/latest/docs/framework/react/reference/hydration#hydrationboundary)
import { QueryClientProvider, HydrationBoundary } from '@tanstack/react-query'
function App() {
return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={pageProps.dehydratedState}>
// ...
</HydrationBoundary>
</QueryClientProvider>
)
}
2. SSR에서 prefetching 데이터를 리액트 쿼리에 캐싱하기
- useQuery의 initialData 속성을 이용하여 서버에서 prefetching한 데이터를 쿼리의 초기 캐시 값으로 사용할 수 있다.
- 하지만 이 방법은 몇가지 단점이 존재한다.
- 동일한 쿼리를 여러 하위 컴포넌트에서 실행 하는 경우에 모든 쿼리에 initialData를 전달하거나, 아니면 initialData를 적용하는 위치를 신중히 고려해야함.
- 서버에서 쿼리가 호출되는 시점을 알 수 없어 refetching 여부는 페이지가 로드된 시점에 따라 결정됨.
- 쿼리 캐시에 이미 데이터가 존재하는 경우에는 initialData로 기존 데이터를 덮어쓰지 않음.
- 'dehydrate/hydrate' API를 사용하면 이러한 문제들이 발생하지 않는다.
- 서버에서 prefetching한 데이터를 직렬화하여 클라이언트에 보내는 것이 아니라, 서버에서 data fetching에 사용한 queryClient 자체를 직렬화하여 클라이언트에서 재사용한다.
// @/app/providers/query-client-provider.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
export const ReactQueryClientProvider = ({ children }: { children: React.ReactNode }) => {
// This ensures each request has its own cache:
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
})
)
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
or
// @/app/providers/query-client-provider.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
})
}
let browserQueryClient: QueryClient | undefined = undefined
function getQueryClient() {
if (typeof window === 'undefined') {
// Server: always make a new query client
return makeQueryClient()
} else {
// Browser: make a new query client if we don't already have one
// This is very important so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
}
export const ReactQueryClientProvider = ({ children }: { children: React.ReactNode }) => {
// NOTE: Avoid useState when initializing the query client if you don't
// have a suspense boundary between this and the code that may
// suspend because React will throw away the client on the initial
// render if it suspends and there is no boundary
const queryClient = getQueryClient()
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
*Suspense query: https://tanstack.com/query/latest/docs/framework/react/guides/ssr#a-quick-note-on-suspense
// @/app/layout.tsx
import { ReactQueryClientProvider } from '@/app/providers/query-client-provider.tsx'
// NEVER DO THIS:
// const queryClient = new QueryClient()
//
// Creating the queryClient at the file root level makes the cache shared
// between all requests and means _all_ data gets passed to _all_ users.
// Besides being bad for performance, this also leaks any sensitive data.
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<ReactQueryClientProvider>
{children}
</ReactQueryClientProvider>
</body>
</html>
)
}
// @/app/page.tsx
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
export default async function Page({ params }: { params: { id: number } }) {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({ queryKey, queryFn })
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<SomeComponents... />
</HydrationBoundary>
)
}
import { useQuery } from '@tanstack/react-query'
function SomeComponent() {
const { data } = useQuery({ queryKe, queryFn })
// This useQuery could just as well happen in some deeper child to
// the <PostsRoute>, data will be available immediately either way
//
// Note that we are using useQuery here instead of useSuspenseQuery.
// Because this data has already been prefetched, there is no need to
// ever suspend in the component itself. If we forget or remove the
// prefetch, this will instead fetch the data on the client, while
// using useSuspenseQuery would have had worse side effects.
// ...
}
참고: https://tanstack.com/query/latest/docs/framework/react/guides/ssr
*Nesting Server components: https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#nesting-server-components
*읽어보기: https://tanstack.com/query/latest/docs/framework/react/guides/prefetching