Reactで日付を表示するとき、サーバーとブラウザのずれを防ぐ方法

​​React​Intl.DateTimeFormat,Playwright,TypeScript,useSyncExternalStore,サーバーレンダリング

React でサーバーレンダリングした日付が、ブラウザでハイドレーションエラーになることがあります。よくある原因の1つは、Intl.DateTimeFormat に言語(ロケール)とタイムゾーンを渡していないことです。両方を指定し、日時の値をサーバーから渡せば、サーバーとブラウザで同じ文字になり、エラーは出なくなります。

筆者は検証環境で、日付の書き方を6通りに変えたページを作りました。サーバーは UTC、ブラウザは東京とニューヨークの設定で開き、日付の文字とエラーを記録しました。

補足

検証環境は Ubuntu 24.04、Node.js 22.22.0 です。使ったのは react / react-dom 19.3.1、typescript 7.0.2、esbuild 0.28.2 です。.tsx を tsc で型チェックしてから esbuild で変換し、開発用ビルドで動かしました。ブラウザは Chromium 141 で、Playwright 1.56.1 で言語とタイムゾーンを設定して開いています。Firefox と Safari では試していません。

言語とタイムゾーンを省くと、実行する場所の設定が使われる

Intl.DateTimeFormat の言語を省くと、実行している環境の既定の言語が使われます。タイムゾーンを省いたときも同じで、実行している環境のタイムゾーンになります(MDN の Intl.DateTimeFormat() コンストラクターの説明)。

サーバーレンダリングでは、同じコンポーネントがサーバーとブラウザの2か所で動きます。サーバーの設定と、閲覧している人のブラウザの設定が同じとは限りません。そのため、指定を省いた書式は、場所によって違う文字を作ります。

検証では、サーバーを TZ=UTC で動かしました。この Node.js の既定は、言語が en-US、タイムゾーンが UTC でした。投稿日時は UTC の 2026-10-23T15:30:00.000Z に固定し、サーバーで HTML の <script> にグローバル変数として書き込み、ブラウザに渡しています。

サーバーとブラウザで、同じ時刻の書式がどう変わったかを3通り並べた図
サーバー(UTC・en-US)とブラウザ(東京・日本語)で、同じ時刻を描いた結果

何も指定しないと、書式も時刻も変わる

まず、言語もタイムゾーンも渡さずに書式を作りました。

tsx
const options = { dateStyle: 'medium', timeStyle: 'short' } as const;

// 言語もタイムゾーンも指定しない
function DefaultTime({ iso }: { iso: string }) {
  const text = new Intl.DateTimeFormat(undefined, options).format(new Date(iso));
  return <time dateTime={iso}>{text}</time>;
}

ブラウザを東京・日本語の設定で開くと、サーバーの HTML は Oct 23, 2026, 3:30 PM で、ブラウザで描いた結果は 2026/10/24 0:30 でした。英語と日本語の違いに加えて、UTC と日本時間の9時間の差で、日付まで変わっています。React 19 はエラーを1件出しました。メッセージの最初の行と差分は次のとおりです(検証の出力から抜き出しました。最初の行に続く原因の候補の一覧は、記録していないため省きました)。

text
Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
    <App scenario="default" postedAt="2026-10-23...">
      <main>
        <h1>
        <p>
          <DefaultTime iso="2026-10-23...">
            <time dateTime="2026-10-23T15:30:00.000Z">
  +           2026/10/24 0:30
  -           Oct 23, 2026, 3:30 PM

+ の行がブラウザ、- の行がサーバーの値です。ハイドレーションのあと、画面の文字はブラウザ側の 2026/10/24 0:30 に置き換わりました。React 19 のハイドレーションエラーの読み方は、「React 19でハイドレーションエラーの表示はどう変わったか」の記事でまとめています。

言語だけ指定しても、タイムゾーンでずれる

次に、言語だけ 'ja-JP' を渡しました。

tsx
// 言語だけ指定する
function LocaleOnlyTime({ iso }: { iso: string }) {
  const text = new Intl.DateTimeFormat('ja-JP', options).format(new Date(iso));
  return <time dateTime={iso}>{text}</time>;
}

書式はそろいましたが、時刻はそろいません。東京の設定のブラウザでは、サーバーの 2026/10/23 15:30 に対して、ブラウザは 2026/10/24 0:30 でした。ニューヨークの設定では 2026/10/23 11:30 です。どちらもハイドレーションエラーが1件出ました。

サーバーとブラウザのタイムゾーンが同じなら、このずれは起きません。開発中のようにサーバーとブラウザを同じパソコンで動かすと、タイムゾーンが同じになるため、この食い違いには気づきにくくなります。

言語とタイムゾーンの両方を指定する

言語とタイムゾーンを両方とも渡すと、どこで動いても同じ文字になります。書式は1か所にまとめておくと、指定の漏れを防げます。

ts
// サーバーとブラウザで同じ文字になるように、言語とタイムゾーンを決めて書式を作る
const dateTimeFormat = new Intl.DateTimeFormat('ja-JP', {
  dateStyle: 'medium',
  timeStyle: 'short',
  timeZone: 'Asia/Tokyo',
});

export function formatDateTime(iso: string): string {
  return dateTimeFormat.format(new Date(iso));
}

コンポーネントからは、この関数を呼ぶだけです。<time> 要素の dateTime 属性には、機械が読める形として元の ISO 文字列を入れています(MDN の time 要素の説明)。

tsx
function FixedTime({ iso }: { iso: string }) {
  return <time dateTime={iso}>{formatDateTime(iso)}</time>;
}

この書き方では、東京とニューヨークのどちらの設定で開いても、サーバーの HTML もハイドレーションのあとの文字も 2026/10/24 0:30 で、エラーは0件でした。

ポイント

Intl.DateTimeFormat には、言語('ja-JP' など)と timeZone('Asia/Tokyo' など)を両方とも渡します。どちらか一方でも省くと、サーバーとブラウザの設定の違いが、そのまま文字の違いになります。

ニューヨークの人にも日本時間で見せることになるので、画面には「日本時間」のように、どのタイムゾーンの時刻かを書き添えておくと親切です。

Node.js と Chromium で、同じ指定の結果を比べた

言語とタイムゾーンをそろえても、format() の結果は実装によって変わってよいことになっています。MDN の format() の説明には、同じ言語でも実装によって出力が違うことがあり、それは仕様で許されている、と書かれています。

そこで、言語とタイムゾーンを指定した4通りの書式を、Node.js 22.22.0 と Chromium 141 で比べました。日本語で dateStyle: 'full' と timeStyle: 'long' を組み合わせた 2026年10月24日土曜日 0:30:00 JST を含め、4通りとも同じ文字でした。確かめたのはこの4通りだけで、Node.js やブラウザの版が違えば結果が変わる可能性は残ります。

今の時刻は、サーバーとブラウザで別々に取らない

言語とタイムゾーンをそろえても、日時の値そのものが違えば食い違います。たとえば、コンポーネントの中で new Date() を呼んで今の時刻を出す場合です。

tsx
// 描くたびに今の時刻を取る
function NowTime() {
  const now = new Date();
  const text = new Intl.DateTimeFormat('ja-JP', {
    dateStyle: 'medium',
    timeStyle: 'medium',
    timeZone: 'Asia/Tokyo',
  }).format(now);
  return <time dateTime={now.toISOString()}>{text}</time>;
}

ブラウザでのハイドレーションを1.5秒遅らせて開くと、東京の設定で、サーバーは 2026/10/23 11:48:44、ブラウザは 2026/10/23 11:48:46 でした。秒が違うため、エラーが1件出ました。差分には dateTime 属性と文字の両方が出ています。

1.5秒の遅れは、極端な条件ではありません。公式リファレンスの、サーバーとブラウザで違う中身を描く方法の注意にも、JavaScript は最初の HTML よりかなり遅れて読み込まれることがある、と書かれています。時刻はサーバーで1回だけ取り、投稿日時と同じように値として HTML に埋め込んでブラウザに渡します。投稿日時をこの形で渡し、言語とタイムゾーンも指定したページでは、食い違いは出ませんでした。

閲覧している人のタイムゾーンで出したいとき

閲覧している人のタイムゾーンで時刻を見せたい場合、サーバーはそのタイムゾーンを知りません。そこで、サーバーでの描画とブラウザでのハイドレーションでは、決めておいたタイムゾーンで描きます。そのあとで、ブラウザのタイムゾーンに切り替えます。

この切り替えには useSyncExternalStore が使えます(基本の使い方は「useSyncExternalStoreで、Reactの外にあるストアを安全に読む方法」の記事で紹介しています)。3つ目の引数の getServerSnapshot は、サーバーでの描画と、ブラウザでのハイドレーションのときだけ使われます(公式リファレンスの、サーバーレンダリングでの使い方)。ブラウザのタイムゾーンは、Intl.DateTimeFormat().resolvedOptions().timeZone で取れます。検証では、この式で Asia/Tokyo と America/New_York が返りました。

ts
import { useSyncExternalStore } from 'react';

// 閲覧している人のタイムゾーンは、途中で変わらないものとして扱う
const subscribe = () => () => {};
const getSnapshot = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
// サーバーと、ブラウザのハイドレーションの間は、決めておいたタイムゾーンを使う
const getServerSnapshot = () => 'Asia/Tokyo';

export function useTimeZone(): string {
  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}

タイムゾーンは、ページを開いている間に変わらないものとして扱いました。そのため subscribe は何もしません。このフックで得たタイムゾーンを、timeZone に渡します。

tsx
function LocalTime({ iso }: { iso: string }) {
  const timeZone = useTimeZone();
  const text = new Intl.DateTimeFormat('ja-JP', {
    ...options,
    timeZone,
  }).format(new Date(iso));
  return (
    <time dateTime={iso}>
      {text}({timeZone})
    </time>
  );
}

ニューヨークの設定のブラウザで開くと、サーバーの HTML は 2026/10/24 0:30(Asia/Tokyo) で、ハイドレーションのあとは 2026/10/23 11:30(America/New_York) に変わりました。エラーは0件です。東京の設定では、切り替えの前とあとが同じ 2026/10/24 0:30(Asia/Tokyo) でした。

この方法では、最初に見えるのはサーバーで決めたタイムゾーンの時刻で、少しあとに切り替わります。公式リファレンスでも、サーバーとブラウザで違う中身を描く方法について、回線が遅いと切り替わりが目立つことがあると注意しています。どのタイムゾーンの時刻かを括弧で添えたのは、切り替わる前の表示を読んだ人が取り違えないようにするためです。

suppressHydrationWarning は、エラーを止めるだけ

suppressHydrationWarning を付けると、その要素の食い違いのエラーは出なくなります。公式リファレンスは、時刻のようにどうしても合わない場合の逃げ道として紹介し、食い違った文字を React は直さない、と書いています(公式リファレンスの、避けられない食い違いのエラーを止める方法)。

最初の「何も指定しない」書式に、suppressHydrationWarning を付けて試しました。

tsx
function SuppressedTime({ iso }: { iso: string }) {
  const text = new Intl.DateTimeFormat(undefined, options).format(new Date(iso));
  return (
    <time dateTime={iso} suppressHydrationWarning>
      {text}
    </time>
  );
}

エラーは0件になりました。ただ、東京の設定のブラウザでも、画面はサーバーの Oct 23, 2026, 3:30 PM のままでした。UTC の英語の時刻が、日本の閲覧者にそのまま見えています。

注意

suppressHydrationWarning はエラーを隠すだけで、表示はサーバーの文字のまま残ります。日付の書式のずれは、言語とタイムゾーンを指定して直すほうが確実です。

確かめた結果の一覧

東京・日本語とニューヨーク・英語の設定のブラウザで、それぞれ開いた結果です。表示はハイドレーションのあとのもので、サーバーは UTC で動かしています。

書き方 東京での表示 ニューヨークでの表示 エラー
何も指定しない 2026/10/24 0:30 Oct 23, 2026, 11:30 AM どちらも1件
言語だけ指定 2026/10/24 0:30 2026/10/23 11:30 どちらも1件
言語とタイムゾーンを指定 2026/10/24 0:30 2026/10/24 0:30 どちらも0件
描くたびに今の時刻を取る 秒がずれた 秒がずれた どちらも1件
ハイドレーションのあとで切り替え 2026/10/24 0:30(Asia/Tokyo) 2026/10/23 11:30(America/New_York) どちらも0件
suppressHydrationWarning を付ける Oct 23, 2026, 3:30 PM Oct 23, 2026, 3:30 PM どちらも0件

エラーが1件出た書き方でも、ハイドレーションのあとの表示はブラウザ側の文字に置き換わっていました。そのため、見た目だけでは食い違いに気づけません。開発中はコンソールのエラーを確かめてください。

​Posted by 86