@jobkaehenry/next-hydrate
v1.0.1
Published
Universal hydration utilities for Next.js App Router and Tanstack-Query/React v5
Downloads
9
Maintainers
Readme
@jobkaehenry/next-hydrate
Universal hydration utilities for Next.js App Router + React Query v5.
🌐 Language / 언어 선택
Table of contents
- Why this library exists
- Installation
- Getting started
- API reference
- Patterns and recipes
- Testing and local development
- FAQ
- Troubleshooting checklist
- Contributing
- License
1. Why this library exists
@jobkaehenry/next-hydrate keeps React Query caches in sync across every rendering strategy offered by the Next.js App Router (SSR, ISR, SSG, CSR). The goal is to ship a single abstraction that:
- detects the runtime automatically using
detectFetchMode()so you can respond to router prefetches, ISR revalidations, or static builds without branching throughout your codebase, - hydrates multiple React Query caches with sensible defaults and payload size guards, and
- exposes ergonomically thin wrappers (
getHydrationProps+withHydration) that fit straight into idiomatic Next.js layouts and route segments.
Internally, the utilities prioritise performance by:
- using a short-lived
QueryClientduring server execution withgcTimetuned down to release memory quickly, - deactivating the React Query logger while prefetching to avoid noisy console output on the server,
- throttling concurrent prefetch operations so that bursty fetch workloads do not overwhelm upstream services, and
- falling back to CSR whenever the dehydrated payload crosses the configured kilobyte threshold.
2. Installation
npm install @jobkaehenry/next-hydrate @tanstack/react-query @tanstack/react-query-devtoolsNote This package expects
next,react, andreact-domto already exist in your project as peer dependencies. Install compatible versions if they are missing.
3. Getting started
3.1 Layout provider
Declare the global QueryProvider once inside your root layout. It keeps a single QueryClient instance alive on the client and optionally mounts the React Query Devtools in development.
// app/layout.tsx
import { QueryProvider } from '@jobkaehenry/next-hydrate';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<QueryProvider>{children}</QueryProvider>
</body>
</html>
);
}3.2 Server data prefetch
When you render a server component, call getHydrationProps with a list of queries. Each query defines a cache key, an async fetcher, and optional overrides (e.g., disabling hydration, limiting infinite query pages, or skipping large payloads via shouldDehydrate).
// app/posts/page.tsx
import { getHydrationProps } from '@jobkaehenry/next-hydrate';
import PostsClient from './PostsClient';
export default async function PostsPage() {
const hydration = await getHydrationProps({
queries: [
{
key: ['posts'],
fetchFn: async () => {
const res = await fetch(`${process.env.API_URL}/api/posts`, {
next: { revalidate: 60, tags: ['posts'] }
});
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
},
},
{
key: ['tags'],
fetchFn: async () => {
const res = await fetch(`${process.env.API_URL}/api/tags`);
if (!res.ok) throw new Error('Failed to fetch tags');
return res.json();
},
},
],
});
return <PostsClient dehydratedState={hydration.dehydratedState} />;
}getHydrationProps automatically skips work when the environment is CSR or when the request is a Next.js router prefetch (next-router-prefetch header). In ISR scenarios the optional revalidate value is forwarded so that your route segment can opt into incremental regeneration with a single flag.
3.3 Client consumption
Wrap any client component that expects hydrated data with the withHydration higher-order component. Your hook code remains unchanged—React Query reuses the server-prefetched caches and behaves as if the queries had just resolved on the client.
// app/posts/PostsClient.tsx
'use client';
import { withHydration } from '@jobkaehenry/next-hydrate';
import { usePostsQuery, useTagsQuery } from '@/lib/queries';
function PostsClientBase() {
const { data: posts } = usePostsQuery();
const { data: tags } = useTagsQuery();
return (
<>
<h3>Tags</h3>
{tags?.map((tag) => (
<span key={tag.id}>{tag.name}</span>
))}
<h3>Posts</h3>
{posts?.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</>
);
}
export default withHydration(PostsClientBase);If you prefer composition over HOCs, import and use the HydrateClient component directly.
// app/posts/PostsClient.tsx
'use client';
import { HydrateClient } from '@jobkaehenry/next-hydrate';
import PostsView from './PostsView';
export default function PostsClient({ dehydratedState }: { dehydratedState: unknown }) {
return (
<HydrateClient state={dehydratedState}>
<PostsView />
</HydrateClient>
);
}3.4 Suspense and streaming tips
- Suspense is enabled by default via the shared
QueryClientconfiguration, so you can wrap any component tree in<Suspense fallback={...}>without extra wiring. - Infinite queries hydrate the specified number of pages (
pagesToHydrate) on the server. Additional pages will be fetched lazily on the client whenfetchNextPageis invoked. - For streaming routes, call
getHydrationPropsbefore returning the initial chunk so the dehydrated payload is ready when<Suspense>boundaries resolve on the client. - Combine with Next.js
route.tshandlers orgenerateStaticParamsto prebuild data while still letting React Query stay authoritative on the client.
4. API reference
detectFetchMode()
| Return value | Description |
| --- | --- |
| "ssr" | Default mode during server-side rendering. |
| "isr" | When the x-next-revalidate header is present. |
| "static" | During static export (build time) when NEXT_PHASE === "phase-production-build". |
| "csr" | Inside the browser or when Next.js sets next-router-prefetch header. |
getHydrationProps(options)
queries: Array of query descriptors.key: Stable query key (tuple or array) used by React Query.fetchFn: Async function returning the data.hydrate(optional): Set tofalseto skip hydration but allow manual prefetching.pagesToHydrate(optional): Limit for infinite queries; defaults to 1.shouldDehydrate(optional): Receives the resolved data and returns a boolean to decide if it should be serialized.
fetchMode(optional): Override auto-detection when you already know the mode.revalidate(optional): Forwarded only in ISR mode.concurrency(optional, default6): Controls how many fetches run in parallel.maxPayloadKB(optional, default200): If the dehydrated payload exceeds this size,dehydratedStatebecomesnullto trigger CSR fallback.devLog(optional, defaultprocess.env.NODE_ENV !== 'production'): Logs[hydrate]diagnostics in development.
Returns an object containing:
dehydratedState: Serializable hydration payload ornullwhen CSR fallback is used.revalidate: Number of seconds for ISR responses. Undefined otherwise.
HydrateClient
Client component that reads state and wraps children in HydrationBoundary when the payload exists. Useful for manual composition.
withHydration(Component)
Higher-order component that injects dehydratedState and renders a <HydrateClient> wrapper automatically.
QueryProvider
Client provider exposing a singleton QueryClient. Mount it once inside the root layout. In non-production environments the React Query Devtools remain available (collapsed by default).
5. Patterns and recipes
- Multiple route segments: Share a single
QueryProvideracross layouts. Each route segment can independently callgetHydrationPropswithout clobbering caches because React Query scopes cache keys per route tree. - Error boundaries: When a server fetch rejects, React Query bubbles the error. Pair the client component with an error boundary or rely on Next.js
error.tsxto gracefully handle failures. - Prefetch cancellation: To opt out of hydration under specific conditions (e.g., feature flag off), return
hydrate: falsefor that query or provide ashouldDehydratethat checks the returned payload size. - Infinite scrolling: Prefetch a handful of pages (
pagesToHydrate: 2) to minimise layout shifts, then rely onfetchNextPagefor subsequent pages. - Streaming partial data: Use
Promise.allto fetch high-priority data eagerly, while low-priority queries can stay in the list withhydrate: falseto avoid blocking the initial response. - Analytics: With
devLogenabled, the console prints mode, query count, payload size, and whether CSR fallback triggered. Pipe these logs to your observability platform during QA.
6. Testing and local development
- Install dependencies with
npm install. - Run unit tests once with
npm run test. - Use
npm run dev:testfor watch mode while editing source files. - Build the package using
npm run build(powered bytsup).
7. FAQ
The package targets v5 APIs. While many behaviours remain compatible with v4, full support is not guaranteed because option names and defaults differ.
Absolutely. Any async function returning data works. makeJsonFetch simply reduces boilerplate for JSON endpoints while wiring Next.js caching hints.
Each layout or page can call getHydrationProps. Pass the resulting dehydratedState down via props to the nearest client component and wrap it with withHydration or HydrateClient.
8. Troubleshooting checklist
| Symptom | Possible cause | Suggested fix |
| --- | --- | --- |
| Hydration mismatch warnings | dehydratedState is null due to payload cap | Increase maxPayloadKB or refine shouldDehydrate. |
| Queries refetch on navigation | Ensure staleTime is configured as desired in your QueryClient. The default is 30s on the client. |
| No data during ISR revalidation | Provide revalidate when calling getHydrationProps so Next.js keeps the cache warm. |
| Fetch runs twice in development | React strict mode intentionally double-invokes fetchers. This is expected behaviour. |
9. Contributing
- Fork the repository and create a feature branch.
- Install dependencies with
npm install. - Run the test suite (
npm run test). - Commit changes following conventional commits.
- Submit a pull request describing the problem and solution. Screenshots or reproduction links are encouraged.
10. License
MIT License
목차
1. 라이브러리 개요
@jobkaehenry/next-hydrate는 Next.js App Router가 제공하는 모든 렌더링 방식(SSR / ISR / SSG / CSR)에서 React Query 캐시를 일관성 있게 유지하기 위한 통합 유틸리티입니다. 핵심 목표는 다음과 같습니다.
detectFetchMode()로 런타임을 자동 감지하여 라우터 prefetch, ISR 재검증, 정적 빌드 상황에서도 조건문을 최소화합니다.- 여러 React Query 쿼리를 한 번에 prefetch하고 페이로드 용량을 측정해 안전하게 하이드레이션합니다.
getHydrationProps와withHydration두 가지 API만으로 Next.js 레이아웃 및 라우트 세그먼트에 자연스럽게 녹아듭니다.
내부적으로는 다음과 같은 성능 최적화가 적용되어 있습니다.
- 서버 실행 시 단기
QueryClient를 생성하고gcTime을 짧게 설정해 메모리를 빠르게 해제합니다. - 서버 콘솔이 지저분해지는 것을 막기 위해 React Query logger를 비활성화합니다.
- 동시 prefetch 개수를 제한하여 백엔드 서비스에 무리가 가지 않도록 합니다.
- 직렬화된 페이로드가 임계치를 넘으면 자동으로 CSR 폴백을 수행합니다.
2. 설치
npm install @jobkaehenry/next-hydrate @tanstack/react-query @tanstack/react-query-devtools참고
next,react,react-dom은 peer dependency입니다. 프로젝트에 존재하지 않는 경우 호환 버전을 함께 설치하세요.
3. 빠른 시작
3.1 레이아웃 Provider 구성
루트 레이아웃에서 QueryProvider를 한 번만 선언하면 전역에서 동일한 QueryClient를 공유하며, 개발 환경에서는 React Query Devtools도 자동으로 마운트됩니다.
// app/layout.tsx
import { QueryProvider } from '@jobkaehenry/next-hydrate';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
<QueryProvider>{children}</QueryProvider>
</body>
</html>
);
}3.2 서버에서 데이터 Prefetch
서버 컴포넌트에서는 getHydrationProps에 쿼리 목록을 전달해 데이터를 선행으로 가져옵니다. 각 항목은 캐시 키, 비동기 fetch 함수, 선택 옵션(hydrate, pagesToHydrate, shouldDehydrate)을 포함합니다.
// app/posts/page.tsx
import { getHydrationProps } from '@jobkaehenry/next-hydrate';
import PostsClient from './PostsClient';
export default async function PostsPage() {
const hydration = await getHydrationProps({
queries: [
{
key: ['posts'],
fetchFn: async () => {
const res = await fetch(`${process.env.API_URL}/api/posts`, {
next: { revalidate: 60, tags: ['posts'] }
});
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
},
},
{
key: ['tags'],
fetchFn: async () => {
const res = await fetch(`${process.env.API_URL}/api/tags`);
if (!res.ok) throw new Error('Failed to fetch tags');
return res.json();
},
},
],
});
return <PostsClient dehydratedState={hydration.dehydratedState} />;
}getHydrationProps는 CSR 환경이나 Next.js 라우터 prefetch 요청(next-router-prefetch 헤더)일 경우 즉시 종료합니다. ISR 상황에서는 revalidate 값을 그대로 전달하므로 간단히 증분 정적 재생성을 켤 수 있습니다.
3.3 클라이언트에서 데이터 사용
하이드레이션이 필요한 클라이언트 컴포넌트를 withHydration HOC로 감싸면 됩니다. 내부 React Query 훅 코드는 그대로 유지되며 서버에서 미리 받아둔 캐시를 즉시 재사용합니다.
// app/posts/PostsClient.tsx
'use client';
import { withHydration } from '@jobkaehenry/next-hydrate';
import { usePostsQuery, useTagsQuery } from '@/lib/queries';
function PostsClientBase() {
const { data: posts } = usePostsQuery();
const { data: tags } = useTagsQuery();
return (
<>
<h3>태그</h3>
{tags?.map((tag) => (
<span key={tag.id}>{tag.name}</span>
))}
<h3>게시글</h3>
{posts?.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</>
);
}
export default withHydration(PostsClientBase);HOC 대신 컴포지션을 선호한다면 HydrateClient 컴포넌트를 직접 사용해도 됩니다.
// app/posts/PostsClient.tsx
'use client';
import { HydrateClient } from '@jobkaehenry/next-hydrate';
import PostsView from './PostsView';
export default function PostsClient({ dehydratedState }: { dehydratedState: unknown }) {
return (
<HydrateClient state={dehydratedState}>
<PostsView />
</HydrateClient>
);
}3.4 Suspense 및 스트리밍 팁
- 기본
QueryClient설정은 Suspense를 활성화하므로<Suspense fallback={...}>만 추가하면 됩니다. - Infinite Query는
pagesToHydrate값만큼 서버에서 페이지를 직렬화합니다. 이후 페이지는fetchNextPage호출 시 클라이언트에서 가져옵니다. - 스트리밍 라우트에서는 첫 청크를 반환하기 전에
getHydrationProps를 호출해<Suspense>경계가 열릴 때 직렬화된 데이터를 바로 사용할 수 있도록 합니다. route.ts핸들러나generateStaticParams와 조합하면 정적 빌드와 React Query 기반 하이드레이션을 동시에 활용할 수 있습니다.
4. API 레퍼런스
detectFetchMode()
| 반환 값 | 설명 |
| --- | --- |
| "ssr" | 기본 서버 사이드 렌더링 모드. |
| "isr" | x-next-revalidate 헤더가 포함된 ISR 상황. |
| "static" | NEXT_PHASE === "phase-production-build"인 정적 빌드 단계. |
| "csr" | 브라우저 환경 또는 next-router-prefetch 헤더가 있는 경우. |
getHydrationProps(options)
queries: 쿼리 설명자 배열key: React Query 캐시 키 (배열 형태 권장)fetchFn: 데이터를 반환하는 비동기 함수hydrate(선택):false로 설정하면 직렬화를 건너뛰면서도 prefetch는 수행합니다.pagesToHydrate(선택): Infinite Query 서버 직렬화 페이지 수 (기본값 1)shouldDehydrate(선택): 반환 데이터 기반으로 직렬화 여부를 결정하는 함수
fetchMode(선택): 이미 모드를 알고 있는 경우 직접 지정revalidate(선택): ISR 모드에서만 응답 헤더에 전달concurrency(선택, 기본 6): 동시 실행할 fetch 개수 제한maxPayloadKB(선택, 기본 200): 직렬화된 페이로드가 이 값을 넘으면dehydratedState를null로 설정하여 CSR로 전환devLog(선택, 기본값은 비프로덕션 환경에서true): 개발용 콘솔 로그 출력
반환 값:
dehydratedState: 직렬화된 하이드레이션 데이터 혹은 CSR 폴백 시nullrevalidate: ISR 응답을 위한 초 단위 재검증 시간 (해당 모드가 아닐 경우undefined)
HydrateClient
state를 읽어 HydrationBoundary로 감싸는 클라이언트 컴포넌트입니다. 직접 컴포지션할 때 유용합니다.
withHydration(Component)
dehydratedState를 주입하고 자동으로 <HydrateClient>로 감싸는 HOC입니다.
QueryProvider
루트 레이아웃에 한 번만 마운트하는 클라이언트 Provider입니다. 개발 환경에서는 React Query Devtools가 함께 표시됩니다(기본은 접힘 상태).
5. 패턴과 레시피
- 여러 라우트 세그먼트: 하나의
QueryProvider를 공유하면서 각 세그먼트에서 독립적으로getHydrationProps를 호출해도 캐시 키가 겹치지 않습니다. - 에러 경계 처리: 서버 fetch가 실패하면 React Query가 오류를 전달합니다. 클라이언트 컴포넌트 주변에 error boundary를 배치하거나 Next.js
error.tsx를 활용하세요. - 조건부 Prefetch: 특정 조건에서 하이드레이션을 건너뛰고 싶다면
hydrate: false를 설정하거나shouldDehydrate에서 데이터를 검사해 결정할 수 있습니다. - Infinite Scroll: 초기 두세 페이지만
pagesToHydrate로 직렬화하고 이후 페이지는fetchNextPage로 로딩하면 레이아웃 흔들림을 줄일 수 있습니다. - 스트리밍과 부분 데이터: 우선순위가 높은 데이터는
Promise.all로 선행 실행하고, 우선순위가 낮은 쿼리는hydrate: false로 설정해 초기 응답을 빠르게 전달합니다. - 모니터링:
devLog가 활성화된 상태에서[hydrate]로그에는 모드, 쿼리 수, 페이로드 용량, CSR 폴백 여부가 표시됩니다. QA 환경에서 로깅 시스템으로 전송하면 상태를 파악하기 쉽습니다.
6. 테스트 & 로컬 개발
npm installnpm run test- 개발 중에는
npm run dev:test로 watch 모드 실행 npm run build로 패키지 번들(tsup 기반)
7. 자주 묻는 질문
본 패키지는 v5 API를 기준으로 제작되었습니다. 일부 기능은 v4에서도 동작할 수 있으나 옵션 명과 기본값이 다르므로 100% 호환을 보장하지 않습니다.
가능합니다. 비동기 데이터만 반환하면 됩니다. makeJsonFetch는 JSON API에서 Next.js 캐싱 옵션을 함께 전달하기 위한 헬퍼일 뿐입니다.
가능합니다. 각 레이아웃 또는 페이지에서 getHydrationProps를 호출하고, 반환된 dehydratedState를 가장 가까운 클라이언트 컴포넌트에 전달하면 됩니다. withHydration 또는 HydrateClient로 감싸면 React Query가 서버 캐시를 그대로 재사용합니다.
8. 트러블슈팅 체크리스트
| 증상 | 원인 | 해결 방법 |
| --- | --- | --- |
| Hydration 경고 발생 | 페이로드가 maxPayloadKB를 초과해 dehydratedState가 null이 됨 | maxPayloadKB를 늘리거나 shouldDehydrate 조건을 조정하세요. |
| 네비게이션 시 매번 refetch | 클라이언트 QueryClient의 staleTime이 짧음 | 레이아웃에서 커스텀 QueryClient 설정을 적용하세요. 기본값은 30초입니다. |
| ISR 재검증 시 데이터 없음 | getHydrationProps 호출 시 revalidate 값을 전달하지 않음 | ISR 페이지는 revalidate 옵션을 지정해 주세요. |
| 개발 모드에서 fetch 두 번 실행 | React Strict Mode의 의도된 동작 | 프로덕션 빌드에서는 한 번만 실행됩니다. |
9. 기여 가이드
- 저장소를 포크하고 작업용 브랜치를 만듭니다.
npm install로 의존성을 설치합니다.npm run test로 테스트를 통과시킵니다.- 변경 사항을 커밋하고 Pull Request를 작성합니다. 문제 원인과 해결 방법을 상세히 적어주세요.
10. 라이선스
MIT License
