🪣

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 で同じデータにアクセスできるようになります。

https://tanstack.com/query/latest/docs/framework/react/guides/ssr

https://tanstack.com/query/v4/docs/framework/react/guides/initial-query-data

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>&copy; 2024 {config.siteName}</p>
  </footer>
);

https://nextjs.org/docs/pages/api-reference/functions/get-initial-props

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>&copy; 2024 {config.siteName}</p>
    </footer>
  );
};

最後に

最後まで読んでくださりありがとうございます!

今回SSR と TanStack Query の Hydrate を組み合わせることで、props のバケツリレーをなくすことを実現できました 👌

個人的な解釈で書いてしまっている部分もあるかもしれないので、もし間違っている箇所があれば教えていただけると嬉しいです 🙏

Social PLUS Tech Blog

Discussion