見出し画像

Next.js の title タグを Client Component から動的に変更する (App Router)

Next.js が App Router になってから、head タグ内の title タグを設定するのは専ら Server Component の責務になっている。

基本は layout.tsx や page.tsx で `export const metadata: Metadata = { title: "Something" }` として静的に定義するか、`export async function generateMetadata() {}` として動的に吐き出す方法がドキュメントで取り扱われている。


今回、"use client" ディレクティブで定義したカスタムフックのロジックを元に title タグを動的に変更したかったため、その実装を行った


(注意)

ここで記載されている内容は title タグを二重で吐き出してしまうので筋が悪い。下記で紹介している document.title = 'new title' で既存の title を上書きする形がよさそうだ


src/components/head-title/provider.tsx

"use client";

import { createContext, useState } from "react";

export const HeadTitleContext = createContext<{
  title: string;
  setTitle: (title: string) => void;
}>({
  title: "",
  setTitle: () => {},
});

export const HeadTitleProvider = ({
  children,
}: { children: React.ReactNode }) => {
  const [title, setTitle] = useState<string>("");
  return (
    <HeadTitleContext.Provider value={{ title, setTitle }}>
      {children}
    </HeadTitleContext.Provider>
  );
};

シンプルなコンテキストを作り・・・


src/components/head-title/index.tsx

"use client";

import { useContext } from "react";
import { HeadTitleContext } from "./provider";

export { HeadTitleProvider } from "./provider";

export const useHeadTitle = () => useContext(HeadTitleContext);

export const HeadTitle = () => {
  const { title } = useContext(HeadTitleContext);
  if (!title) return null;
  return <title>{title}</title>;
};

それを元に吐き出す HeadTitle コンポーネントとカスタムフックを作る


src/app/layout.tsx

import { HeadTitle, HeadTitleProvider } from "@/components/head-title";

export { metadata } from "./metadata";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <HeadTitleProvider>
      <html lang="en">
        <head>
          <HeadTitle />
        </head>
        <body>
          {children}
        </body>
      </html>
    </HeadTitleProvider>
  );
}

あとはコンテキストプロバイダーで html を囲み、 html タグ直下に head > HeadTitle コンポーネントを差し込むだけで準備ができる

なお、HeadTitle で title が存在しない場合 null を返すが、これによって従来の metadata や generateMetadata の設定を使うこともできる。コンテキストの title が存在するとそれらの設定を上書きするという仕様になる。


この仕組みを拡張すれば、title タグ以外のメタタグも自由にクライアントコンポーネントで拡張できる。

Next.js の Discussion では document.title を直接操作する方法なども紹介されている。

How to set page title and description if page is "use client" ? · vercel/next.js · Discussion #50872


使用方法

"use client";

import { useHeadTitle } from "@/components/head-title";
import { useTimer } from "@/components/timer/use-timer";
import { useEffect } from "react";

export const HeadTitleController = () => {
  const { setTitle } = useHeadTitle();
  const { time, status } = useTimer();

  useEffect(() => {
    if (status === "running") {
      setTitle(`${time.mm}:${time.ss} - Web Timer`);
    } else {
      setTitle("");
    }
  }, [status, time.mm, time.ss, setTitle]);

  return null;
};

元のタイトルに戻したい場合は `setTitle("")` をするだけで良い

この方法ではコンポーネントを用意しているが、もちろんカスタムフック内での仕様も可能だ


いいなと思ったら応援しよう!