Next.js(Pages Router)× TanStack Query Hydrate で SSR のバケツリレー問題解消!
こんにちは、ずっきーです!
実務の中で「ページの初期表示を速くしたい!」という課題があり、「サイト設定(サイト名、サイトのロゴ画像など)」 の取得を SSR(サーバーサイドレンダリング) で行うようにしました。
このアプリは少し特殊で複数のサイトを出し分けるので、 API でサイト設定を取得してます ☝️
しかし、取得したサイト設定を Layout や Footer などの非ページコンポーネントでも使いたい場面が多く、props による受け渡しが多くなってしまいました。いわゆるバケツリレー問題に悩まされました、、
Next.js の App Router であれば、 Server Component を使ってすんなり解決できるのかもしれませんが、 Pages Router を採用しているため何かいい方法がないかと検討した結果、TanStack Query の Hydrate を使って解決できたので記事にしたいと思います。
TanStack Query の Hydrate とは?
簡単に言うと、 SSR で取得したデータを、クライアント側でそのまま再利用できるようにする仕組みです!
Hydrate を使えば、 SSR で取得したデータを TanStack Query のキャッシュとしてそのままクライアントに引き継ぐことができ、どのコンポーネントからでも useQuery で同じデータにアクセスできるようになります。
Hydrate を使わない場合と使った場合を比較 ⭐
Hydrate を使わない場合(例としてgetInitialProps での直接API呼び出し)
getInitialProps で API を叩き、取得したデータを props としてコンポーネントに受け渡していくと、バケツリレーが発生してしまいました。
下の例では、Header や Footer コンポーネントにて config(サイト設定)を使いたい場合は、Layout コンポーネントからさらに props での受け渡しが発生しているのがわかるかと思います!
実装方法
// _app.tsx
const MyApp = ({ Component, pageProps, config }) => (
<Layout config={config}>{/* ❌ バケツリレー発生 */}
<Component {...pageProps} />
</Layout>
);
MyApp.getInitialProps = async (context: AppContext) => {
// 直接APIを呼び出し
const response = await fetch(`/api/config`);
const config = await response.json();
return {
config,
};
};
コンポーネントでのデータ使用
// Layout.tsx
const Layout = ({ children, config }) => (
<div>
<Header config={config} />{/* ❌ さらにHeaderにpropsを渡す */}
<main>{children}</main>
<Footer config={config} />{/* ❌ さらにFooterにpropsを渡す */}
</div>
);
// Header.tsx - 実際にconfigを使用するコンポーネント
interface HeaderProps {
config: SiteConfig;
}
const Header: React.FC<HeaderProps> = ({ config }) => (
<header>
<img src={config.logoUrl} alt={config.siteName} />
<h1>{config.siteName}</h1>
</header>
);
// Footer.tsx - 実際にconfigを使用するコンポーネント
interface FooterProps {
config: SiteConfig;
}
const Footer: React.FC<FooterProps> = ({ config }) => (
<footer>
<p>© 2024 {config.siteName}</p>
</footer>
);
Hydrate を使った場合の実装
TanStack Query の Hydrate を使用することで、バケツリレー問題を解決できました ㊗️
またオマケとしてバケツリレー解消以外のメリットとして以下もありそうだなと思いました 👇
- クライアント側のコードは通常通り useQuery を書くだけなので使う側では特に意識する必要がなく使いやすい
- SSR をやめたくなった場合は prefetchQuery を消すだけで済むので移行がラク、また、他の API も SSR で取得したい場合でも簡単に追加できる
実装方法
// _app.page.tsx
import { dehydrate, Hydrate, QueryClient } from '@tanstack/react-query';
const MyApp = ({ Component, pageProps, dehydratedState }) => (
<ReactQueryProvider>
{/* サーバーで作ったキャッシュを復元 */}
<Hydrate state={dehydratedState}>
<Layout>
<Component {...pageProps} />
</Layout>
</Hydrate>
</ReactQueryProvider>
);
MyApp.getInitialProps = async (context: AppContext) => {
const ctx = await App.getInitialProps(context);
// サイト設定を SSR で事前取得
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['siteConfig'],
queryFn: async () => {
const response = await fetch(`/api/config`);
return response.json();
},
});
return {
...ctx,
dehydratedState: dehydrate(queryClient), // クライアントへ渡す
};
};
コンポーネントでのデータ使用
// Layout.tsx - propsでデータを受け取る必要がない
interface LayoutProps {
children: React.ReactNode;
// ✅ configのpropsが不要!
}
const Layout: React.FC<LayoutProps> = ({ children }) => (
<div>
<Header /> {/* ✅ propsの受け渡し不要 */}
<main>{children}</main>
<Footer /> {/* ✅ propsの受け渡し不要 */}
</div>
);
// Header.tsx - 必要な場所で直接useQueryを使用
const Header: React.FC = () => {
const { data: config, isLoading } = useQuery({
queryKey: ['siteConfig'],
queryFn: async () => {
const response = await fetch(`/api/config`);
return response.json();
},
});
// ✅ SSRで取得済みの場合、即座にデータが利用可能
return (
<header>
<img src={config.logoUrl} alt={config.siteName} />
<h1>{config.siteName}</h1>
</header>
);
};
// Footer.tsx - 必要な場所で直接useQueryを使用
const Footer: React.FC = () => {
const { data: config } = useQuery({
queryKey: ['siteConfig'], // ✅ 同じqueryKeyなので既存のキャッシュを利用
queryFn: async () => {
const response = await fetch(`/api/config`);
return response.json();
},
});
return (
<footer>
<p>© 2024 {config.siteName}</p>
</footer>
);
};
最後に
最後まで読んでくださりありがとうございます!
今回SSR と TanStack Query の Hydrate を組み合わせることで、props のバケツリレーをなくすことを実現できました 👌
個人的な解釈で書いてしまっている部分もあるかもしれないので、もし間違っている箇所があれば教えていただけると嬉しいです 🙏
Discussion