React 19で、titleやmetaをコンポーネントの中に書く方法
Reactmeta,title,TypeScript,サーバーレンダリング
React 19 では、<title> や <meta> を、ページの部品(コンポーネント)の JSX にそのまま書けます。どこに書いても、React が <head> の中へ移してくれるので、useEffect で document.title を書き換えたり、ライブラリを入れたりしなくても、ページごとのタイトルと説明文を出せます。
筆者は検証環境で、ページを切り替えたときの <head> の中身、index.html に最初からある <title> との関係、2つの部品が <title> を書いたときの動き、空のタイトルになってしまう書き方、<head> に移されない書き方、サーバーで描いたときの出力を確かめました。
検証環境は Ubuntu 24.04、Node.js 22.22.0 です。使ったのは react / react-dom 19.3.1、@types/react / @types/react-dom 19.3.0、typescript 7.0.2、esbuild 0.28.2、jsdom 29.1.1 です。.tsx を tsc で型チェックしてから esbuild で変換し、jsdom の上で、開発用ビルドの React で動かしました。GIF は、同じコンポーネントを Playwright 1.56.1 の Chromium で動かして撮ったものです。
ページの部品の中に title と meta を書く
React 19 の発表記事にある、ドキュメントのメタデータについての節によると、これまでは <title>、<link>、<meta> を <head> に入れるには、エフェクトの中で手で入れるか、react-helmet のようなライブラリを使う必要がありました。React 19 からは、部品の中に書いたこれらのタグを、React が <head> へ移します。
次の Pages は、ボタンで2つのページを切り替えます。それぞれのページの部品が、<article> の中に自分の <title>、説明文の <meta>、正規の URL を示す <link rel="canonical"> を書いています。
import { useState } from 'react';
// ページごとの部品が、自分の title・meta・link を描く
function HomePage() {
return (
<article>
<title>トップ | 猫の手ブログ</title>
<meta name="description" content="猫の手ブログのトップページです。" />
<link rel="canonical" href="https://example.com/" />
<h1>トップ</h1>
</article>
);
}
export function CatPage() {
return (
<article>
<title>猫の記事 | 猫の手ブログ</title>
<meta name="description" content="猫についての記事です。" />
<link rel="canonical" href="https://example.com/cats" />
<h1>猫の記事</h1>
</article>
);
}
export function Pages() {
const [page, setPage] = useState<'home' | 'cats' | 'none'>('home');
return (
<>
<nav>
<button onClick={() => setPage('home')}>トップ</button>
<button onClick={() => setPage('cats')}>猫の記事</button>
<button onClick={() => setPage('none')}>どちらも出さない</button>
</nav>
{page === 'home' && <HomePage />}
{page === 'cats' && <CatPage />}
</>
);
}
Vite の React テンプレートの index.html では、<head> に最初から <title> が書かれています。この検証でも、<head> に <title>初期のタイトル</title> を書いた HTML の上で描きました。最初に描いたあとの <head> と、#root の中身は次のとおりです。
--- 最初の描画
document.title = "トップ | 猫の手ブログ"
head: <meta charset="utf-8"><title>トップ | 猫の手ブログ</title><title>初期のタイトル</title><meta name="description" content="猫の手ブログのトップページです。"><link rel="canonical" href="https://example.com/">
body: <nav><button>トップ</button><button>猫の記事</button><button>どちらも出さない</button></nav><article><h1>トップ</h1></article>
3つのタグは <article> の中から消え、<head> に入っています。<head> に最初からあった <title>初期のタイトル</title> は消えずに残り、React の <title> はその前に入りました。HTML の仕様の document.title の定義では、document.title は文書の中で最初に出てくる <title> の文字を返します。そのため、React の書いたタイトルになっています。
ページを切り替えると、head の中身も入れ替わる
ボタンを押してページを切り替えると、<head> の中身もページに合わせて入れ替わります。次の GIF は、同じ Pages を Chromium で動かし、押すたびに document.title と <head> の中身を書き出したものです。

jsdom で同じ順に押したときの出力のうち、1回目と2回目のあとの document.title と <head> を写します。
--- 「猫の記事」を押したあと
document.title = "猫の記事 | 猫の手ブログ"
head: <meta charset="utf-8"><title>猫の記事 | 猫の手ブログ</title><title>初期のタイトル</title><meta name="description" content="猫についての記事です。"><link rel="canonical" href="https://example.com/cats">
--- 「どちらも出さない」を押したあと
document.title = "初期のタイトル"
head: <meta charset="utf-8"><title>初期のタイトル</title>
ページの部品がなくなると、その部品の書いたタグも <head> から消えました。<title> を書く部品が1つもないときは、残っていた 初期のタイトル が document.title に戻ります。アプリ全体をアンマウントしたときも同じでした。index.html の <title> は、React の部品がタイトルを書かないあいだの代わりとして働きます。
2つの部品が title を書くと、両方が head に入る
サイト共通のレイアウトと、その中のページの両方が <title> と説明文の <meta> を書くと、どうなるかを試しました。
import type { ReactNode } from 'react';
// 共通のレイアウトと、その中のページの両方が title と description を描く
function Layout({ children }: { children: ReactNode }) {
return (
<div className="layout">
<title>猫の手ブログ</title>
<meta name="description" content="サイト共通の説明です。" />
{children}
</div>
);
}
function CatPage() {
return (
<article>
<title>猫の記事 | 猫の手ブログ</title>
<meta name="description" content="猫についての記事です。" />
<h1>猫の記事</h1>
</article>
);
}
export function TwoTitles() {
return (
<Layout>
<CatPage />
</Layout>
);
}
--- 最初の描画
document.title = "猫の記事 | 猫の手ブログ"
head: <meta charset="utf-8"><title>猫の記事 | 猫の手ブログ</title><title>猫の手ブログ</title><title>初期のタイトル</title><meta name="description" content="サイト共通の説明です。"><meta name="description" content="猫についての記事です。">
React は、どちらかを選んで片方を消すことはしません。<title> は3つ、description の <meta> は2つ、<head> に並びました。この検証では document.title がページの側のタイトルになりましたが、<title> のリファレンスの注意書きには、複数の部品が同時に <title> を書いたときのブラウザや検索エンジンの動きは決まっていない、とあります。並び順に頼らず、<title> を書く部品は、そのとき1つだけにしておきます。
ルートに合わせて共通の値を上書きしたいときは、ライブラリのほうが向いています。発表記事の補足でも、単純な使い方ならタグを書くだけで足りるが、ルートに応じて共通のメタデータを上書きするような機能はライブラリのほうが強い、と書かれています。
title の中で {} を使うと、空のタイトルになる
ページ番号のような値をタイトルに混ぜるとき、JSX の {} をそのまま使うと、タイトルが空になります。
import { useState } from 'react';
// 🔴 {} で数を混ぜると、children が文字列1つにならない
export function TitleWithNumber() {
const [page, setPage] = useState(1);
return (
<>
<title>検索結果 {page}ページ目</title>
<button onClick={() => setPage(page + 1)}>次へ</button>
</>
);
}
--- 最初の描画
document.title = ""
head: <meta charset="utf-8"><title></title><title>初期のタイトル</title>
body: <button>次へ</button>
--- 「次へ」を押したあと
document.title = ""
head: <meta charset="utf-8"><title></title><title>初期のタイトル</title>
body: <button>次へ</button>
<title> のリファレンスの、変数をタイトルに使う節によると、<title> の children は文字列1つでなければなりません。検索結果 {page}ページ目 と書くと、children は文字列と数を並べた配列になります。
ブラウザ向けの描画(react-dom/client)では、開発用ビルドでも警告が出ず、<title> が空のまま <head> に入りました。index.html のタイトルより前に入るため、document.title は空になります。
同じ部品をサーバーで描くと(renderToString)、開発用ビルドでは次のエラーがコンソールに出ました(長いので、最初の2文だけを写しています)。
[console.error] React expects the `children` prop of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found an Array with length 3 instead. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert `children` of <title> tags to a single string value which is why Arrays of length greater than 1 are not supported.
この文言を含むファイルを react-dom の開発用ビルドの中で探すと、見つかったのはサーバー向けのファイルだけでした。サーバーで描かないアプリでは、この警告に頼れません。
直すには、テンプレートリテラルで文字列1つにしてから渡します。次のコードは、さきほどの TitleWithNumber と同じファイルの続きで、useState はその先頭で読み込んでいます。
// テンプレートリテラルで、文字列1つにしてから渡す
export function TitleWithTemplate() {
const [page, setPage] = useState(1);
return (
<>
<title>{`検索結果 ${page}ページ目`}</title>
<button onClick={() => setPage(page + 1)}>次へ</button>
</>
);
}
--- 最初の描画
document.title = "検索結果 1ページ目"
--- 「次へ」を押したあと
document.title = "検索結果 2ページ目"
head に移されないもの
<title> のリファレンスと<meta> のリファレンスには、<head> へ移さない例外が書かれています。<svg> の中の <title> は文書のタイトルではなく図の説明なので、そのまま残ります。itemProp を付けた <title> や <meta> も、文書全体ではなくページの一部についての情報なので、書いた場所に残ります。
// head に移されないもの:svg の中の title と、itemProp を付けた meta
export function Exceptions() {
return (
<section itemScope itemType="https://schema.org/Product">
<svg width="24" height="24" role="img">
<title>肉球のアイコン</title>
<circle cx="12" cy="12" r="10" />
</svg>
<meta itemProp="name" content="猫じゃらし" />
<h2>猫じゃらし</h2>
</section>
);
}
--- 最初の描画
document.title = "初期のタイトル"
head: <meta charset="utf-8"><title>初期のタイトル</title>
body: <section itemscope="" itemtype="https://schema.org/Product"><svg width="24" height="24" role="img"><title>肉球のアイコン</title><circle cx="12" cy="12" r="10"></circle></svg><meta itemprop="name" content="猫じゃらし"><h2>猫じゃらし</h2></section>
どちらも <section> の中に残り、<head> は変わりませんでした。なお、<link> のリファレンスによると、<link rel="stylesheet"> は precedence を付けたときだけ <head> に移され、onLoad や onError を付けた <link> は移されません。スタイルシートの扱いは、この記事では試していません。
サーバーで描いたときの出力
<html> から描くなら、サーバーで描くときも、同じ書き方で <head> に入ります。次の Document を、react-dom/server の renderToReadableStream で HTML にしました。<head> には、文字コードの <meta> だけを書いています。
import { CatPage } from './Pages';
// サーバーで描く文書全体。head には charset だけを書いておく
export function Document() {
return (
<html lang="ja">
<head>
<meta charSet="utf-8" />
</head>
<body>
<CatPage />
</body>
</html>
);
}
--- 1. html から描いた文書全体を renderToReadableStream で
<!DOCTYPE html><html lang="ja"><head><meta charSet="utf-8"/><title>猫の記事 | 猫の手ブログ</title><meta name="description" content="猫についての記事です。"/><link rel="canonical" href="https://example.com/cats"/></head><body><article><h1>猫の記事</h1></article></body></html>
<body> の中の CatPage が書いた3つのタグが、<head> の中に出力されました。ページごとのタイトルと説明文が、サーバーが返す HTML の <head> に最初から入ります。
<html> を含まない CatPage だけを renderToString で描くと、3つのタグは <article> の外に出て、出力の先頭に並びました。
--- 2. ページの部品だけを renderToString で
<title>猫の記事 | 猫の手ブログ</title><meta name="description" content="猫についての記事です。"/><link rel="canonical" href="https://example.com/cats"/><article><h1>猫の記事</h1></article>
<title>、<meta>、<link> は、ページの部品の中に書けば、ブラウザでも、<html> から描くサーバーの描画でも、React が <head> に入れます。<title> は、その画面で1つの部品だけが書き、中身はテンプレートリテラルで文字列1つにして渡します。
Posted by 86
関連記事

useDeferredValueで重い一覧による入力の遅れを防ぐ方法
入力のたびに重い一覧を描き直すと、文字が出るまで待たされます。useDeferr ...

lazyとSuspenseでコンポーネントを後から読み込む方法
lazyで包んだコンポーネントは、最初に表示するときまでコードを読み込みません。 ...

keyを変えて、Reactコンポーネントの状態をリセットする方法
Reactは、同じ場所に描画されたコンポーネントのstateを引き継ぎます。ke ...

React 19で、refのコールバックに後片付けの関数を返す方法
React 19では、refのコールバックから関数を返すと、要素が外れるときにR ...

React 19のuseでPromiseとコンテキストを読む方法
React 19のuseなら、Promiseとコンテキストを描画中に読めます。待 ...
