Next.js でサーバー・クライアント間で Date オブジェクトを扱うときの注意点

公開日:

目次

Next.js で Date オブジェクトに変換した日付のデータをサーバーコンポーネントとクライアントコンポーネントで利用している場合の対応に戸惑ったので、原因と対応方法を備忘録としてブログに残しておきます。

今回、Date オブジェクトを利用していて困った点は次の点になります。

  • ローカル環境と本番環境でタイムゾーンが異なるため、本番環境でのみ9時間後の日付を表示してしまう
  • Text content does not match server-rendered HTML. エラーが発生する

上記2つの点について原因と解決方法を説明していきたいと思います。

ローカル環境と本番環境でタイムゾーンが異なるため、本番環境でのみ9時間後の日付を表示してしまう

ブログ記事の投稿日などの日付情報を表示する場合、SSGやSSRを用いてサーバー側で取得した日付データを Dateオブジェクトに変換してクライアントコンポーネントに渡して表示すると、本番環境でのみ9時間後の日付を表示してしまうといった不具合が発生します。

例えば、12月19日 16時30分に投稿した記事が 12月20日 1時30分に投稿した記事として表示されてしまいます。

特にSSGを利用しているページで相対時間表記を採用している場合、クライアントコンポーネントとして表示する必要があり、不具合となる可能性があります。

原因

これはサーバーのタイムゾーンがローカル環境ではタイムゾーンが Asia/Tokyo として実行されており、本番環境(Vercel)では、タイムゾーンが UTCとして実行されていることが原因です。

以下のコードのようにサーバーで作成したDateオブジェクトをPropsとしてクライアントコンポーネントに引き渡す場合、本番環境では、サーバーではタイムゾーンが UTC として扱われていたDateオブジェクトがブラウザではタイムゾーンが Asia/Tokyo としてレンダリングされるため9時間後の日時を表示してしまうのです。

'use client';

type Props = Omit<React.ComponentProps<'time'>, 'dateTime'> & {
  createdAt: Date;
};

export default function CreatedTime({ createdAt, className }: Props) {
  const now = new Date();
  return (
    <time dateTime={formatISO9075(createdAt)} className={className}>
      {formatCreatedAt(createdAt, now)} // formatCreatedAtは自作のフォーマット用関数
    </time>
  );
}

ローカル環境でも npm run dev でローカルサーバーを起動するときにタイムゾーンを UTC として設定することで本番環境の現象を再現することができます。

// package.json

"scripts": {
  "dev": "TZ='Etc/UTC' next dev",
  ...
},

解決方法

サーバー・クライアント間で日付情報を共有する場合は Date オブジェクトではなく文字列として共有するようにすることで、Dateオブジェクトをサーバー・クライアントそれぞれのタイムゾーンで生成し解決することができました。

'use client';

type Props = Omit<React.ComponentProps<'time'>, 'dateTime'> & {
  createdAt: string;
};

export default function CreatedTime({ createdAt, className }: Props) {
  const now = new Date();
  return (
    <time dateTime={formatISO9075(new Date(createdAt))} className={className}>
      {formatCreatedAt(new Date(createdAt), now)}
    </time>
  );
}

この CreatedTime コンポーネントを利用する場合は、Date オブジェクトではなく、文字列として日付情報を引き渡します。

<CreatedTime
  createdAt={formatISO9075(new Date('2023-12-19 16:30:42'))}
/>

今回は日付情報をISO9075の形式にフォーマットしてクライアントコンポーネントに渡していますが、タイムゾーンを含んだ文字列として渡す方が良かったかもと思っています。

Text content does not match server-rendered HTML. エラーが発生する

日付情報を `'use client'` を利用してクライアントコンポーネントとして表示すると、以下のようなエラーがコンソールに表示されます。

Text content does not match server-rendered HTML. 

原因

このエラーは、ReactによってレンダリングされたHTMLが、サーバー・クライアント間で異なることが原因で発生します。

日付のデータは、タイムゾーンの関係でサーバーとクライアント間で異なってしまうため React がエラーを発生させてしまいます。

Next.js のドキュメントでこのエラーについての原因と解決方法が記されています。

解決方法

今回は next/dynamic を利用することによりエラーを解決しています。SSGのビルド時ではなくブラウザで表示されたときに相対時間の計算を行う必要があったためです。

import dynamic from 'next/dynamic';

const CreatedTime = dynamic(
  () => import('@/path/to/components/CreatedTime').then((mod) => mod.CreatedTime),
  { ssr: false },
);

今回は next/dynamic を利用することで解決しましたが、suppressHydrationWarning を利用することでも解決できるようです

export default function CreatedTime({ createdAt, className }: Props) {
  const now = new Date();
  return (
    <time dateTime={formatISO9075(new Date(createdAt))} suppressHydrationWarning className={className}>
      {formatCreatedAt(new Date(createdAt), now)}
    </time>
  );
}

まとめ

今回は Next.js で Date オブジェクトを扱うときの注意点をブログにしました。Next.js はサーバー・クライアントの区別を考えることなくReactでWebサイトを制作できるが故に日付の表示に手間取りました。