🍆

【Next.js + Supabase】動的にmetaを出力する際に2回データ取得してしまう問題を解決する

2024/05/03に公開

Next.jsで動的にメタデータを出力する際には下記のような記述を行います。

page.tsx
// 動的にメタデータを出力する
export async function generateMetadata({ params }: { params: { id: string } }) {
  const data = await getPostData(params.id);

  return {
    title: data.title,
    description: data.description
  };
}

// ブラウザ画面に出力する
export default async function MyPage({ params }: { params: { id: string } }) {
  const data = await getPostData(params.id);

  return (
    <>
      <p>{data.title}</p>
    </>
  );
}

この記述には問題があります。

【問題】データ取得が2回行われてしまっている

const data = await getPostData(id); でデータ取得を行っているのですが、 generateMetadata 内で1回、 MyPage で1回、合計2回も行われています。

どちらも同じデータを取得しているのに、謎に2回もリクエストしている影響でサーバーサイドに余分な負荷がかかってしまいます。メタデータのためだけに余分なリクエストを投げるのは勿体ないので、1回だけで済ませる方法をご紹介します!

※この記事は下記を参考にさせて頂きました。(参考というか丸パクですね...w)
https://zenn.dev/yu_ta_9/articles/e28da9cb6febdb

【解決】キャッシュを使おう

下記のように記述をするとキャッシュが効き、1回のリクエストに抑えることができます。

page.tsx
import { cache } from "react";
import { notFound } from "next/navigation";
import { useSupabase } from "@/hooks/useSupabase";
import type { Database } from "@/types/supabase-schema";

export const dynamic = "force-dynamic"; // SSR

// GetMyPost関数をキャッシュが効くようにする
const getPostInfo = cache(async (id: string) => {
  const postInfo = await GetMyPost(id);
  return postInfo;
});

export async function generateMetadata({ params }: { params: { id: string } }) {
  const postInfo = await getPostInfo(params.id);

  return {
    title: postInfo[0].title,
    description: postInfo[0].description
  };
}

export default async function PostPage({ params }: { params: { id: string } }) {
  const postInfo = await getPostInfo(params.id);

  return (
    <>
      <h1>{postInfo[0].title}</h1>
      <p>{postInfo[0].contents}</p>
    </>
  );
}

/** ================================================ **/
// 下記でSupabaseからデータ取得をしている。
// 個人的な考えで .select() を使わずに `.rpc()` を使っていますが、selectとほぼ同じだとお考えください。
/** ================================================ **/
type GetMyPostType = Database["public"]["Functions"]["get_my_post_by_id"]["Returns"];
async function GetMyPost(id: string): Promise<GetMyPostType> {
  const { supabase } = useSupabase();

  const { data, error } = await supabase.rpc(`get_my_post_by_id`, {
    p_id: id,
  });

  if (error || data.length === 0) {
    notFound();
  }

  return data;
}

【ポイント1】cache を使おう

const getPostInfo = cache(async (id: string) => {
  const postInfo = await GetMyPost(id);
  return postInfo;
});

React標準のモジュールの cache で囲むだけでリクエスト単位でキャッシュ化できます。とてもシンプルにキャッシュ化できますね!

【ポイント2】データ取得はcache関数を定義したものを使う

const postInfo = await getPostInfo(params.id); この記述が2回出てきておりますが、これは「ポイント1」で挙げたキャッシュ関数を利用しているため、何度呼び出されてもキャッシュからデータを提供してくれるようになっています。

間違っても GetMyPost() をそのまま叩かないように注意してください。2回リクエストが行われてしまいます。

【付録】本当に2回リクエストされてないか確認したい

async function GetMyPost(id: string): Promise<GetMyPostType> {
  const { supabase } = useSupabase();

  const { data, error } = await supabase.rpc(`get_my_post_by_id`, {
    p_id: id,
  });

  if (error || data.length === 0) {
    notFound();
  }

  console.log("リクエストされたよ!");

  return data;
}

上記のように console.log() を追加し、ターミナルで確認してみてください。「リクエストされたよ!」が一度しか表示されないはずです。ということは、問題なくキャッシュ関数が効いているということが確認できます。

さいごに

Supabaseはとても簡単に利用できますが、こういう細かいところが戸惑うポイントですね。皆さんも細かいところをどんどん記事にしていき日本のSupabaseユーザーコミュニティを盛り上げていきましょう!

ノウハウ積みまくろ!

Discussion