Next.js(Pages Router)v16でタイムゾーンを処理する方法

ユーザーのローカルタイムゾーンで時間を表示する

問題

アプリケーションがユーザーの所在地を考慮せずに時刻を表示すると、混乱が生じます。「午後3時」に予定されている会議は、閲覧者がニューヨーク、ロンドン、東京のどこにいるかによって異なる瞬間を意味します。サーバーがUTCや自身のタイムゾーンで時刻をレンダリングすると、他の地域のユーザーは不正確なローカル時間を見ることになり、予定の見落としやスケジューリングエラーにつながります。同じタイムスタンプでも、各ユーザーのタイムゾーンに基づいて異なる解釈が必要です。

解決策

タイムスタンプをISO 8601やUnixタイムスタンプなどの普遍的な形式で保存し、クライアント側でユーザーのローカルタイムゾーンに合わせて表示用にフォーマットします。ブラウザの国際化APIを使用してユーザーのタイムゾーンを検出し、それをreact-intlのフォーマット関数に渡します。これにより、すべてのユーザーが自分のタイムゾーンに調整された時刻を見ることができ、イベントがいつ発生するかについての曖昧さがなくなります。

ステップ

1. クライアント側でユーザーのタイムゾーンを検出する

ブラウザのIntl.DateTimeFormat().resolvedOptions().timeZoneは、America/New_YorkEurope/LondonなどのユーザーのIANAタイムゾーン識別子を返します。この値にアクセスするためのカスタムフックを作成します。

import { useState, useEffect } from "react";

export function useUserTimeZone() {
  const [timeZone, setTimeZone] = useState<string>("UTC");

  useEffect(() => {
    const detected = Intl.DateTimeFormat().resolvedOptions().timeZone;
    setTimeZone(detected);
  }, []);

  return timeZone;
}

このフックはクライアント側でのみ実行され、ハイドレーションの不一致を避け、タイムゾーンが検出されるまでデフォルトでUTCを使用します。

2. FormattedDateを使用してユーザーのタイムゾーンで日付をフォーマットする

react-intlのフォーマットメソッドにtimeZoneオプションを渡して、表示に使用するタイムゾーンを制御します。検出されたタイムゾーンで<FormattedDate>コンポーネントを使用します。

import { FormattedDate } from "react-intl";
import { useUserTimeZone } from "../hooks/useUserTimeZone";

interface EventDateProps {
  timestamp: string;
}

export function EventDate({ timestamp }: EventDateProps) {
  const userTimeZone = useUserTimeZone();

  return (
    <FormattedDate
      value={new Date(timestamp)}
      timeZone={userTimeZone}
      year="numeric"
      month="long"
      day="numeric"
      hour="numeric"
      minute="2-digit"
      timeZoneName="short"
    />
  );
}

timeZoneプロパティにより、日付はサーバーやUTCではなく、ユーザーのローカルタイムゾーンでフォーマットされます。

3. useIntlを使用して命令的に時間をフォーマットする

コンポーネントではなくフォーマット済みの文字列が必要なシナリオでは、useIntlフックを使用して命令的なフォーマットAPIにアクセスします。

import { useIntl } from "react-intl";
import { useUserTimeZone } from "../hooks/useUserTimeZone";

interface MeetingTimeProps {
  startTime: string;
}

export function MeetingTime({ startTime }: MeetingTimeProps) {
  const intl = useIntl();
  const userTimeZone = useUserTimeZone();

  const formattedTime = intl.formatDate(new Date(startTime), {
    timeZone: userTimeZone,
    hour: "numeric",
    minute: "2-digit",
    timeZoneName: "short",
  });

  return <span title={startTime}>{formattedTime}</span>;
}

formatDateメソッドは、timeZoneを含むDateTimeFormatOptionsを第2引数として受け取り、タイムスタンプの表示方法を完全に制御できます。

4. サーバータイムスタンプをISO文字列として渡す

getServerSidePropsでは、リクエスト時にデータを取得し、ページコンポーネントにプロップスとして渡します。タイムゾーン情報を保持するために、日付をISO 8601文字列としてシリアライズします。

import { GetServerSideProps } from "next";

interface Event {
  id: string;
  title: string;
  startTime: string;
}

interface EventPageProps {
  event: Event;
}

export const getServerSideProps: GetServerSideProps<
  EventPageProps
> = async () => {
  const event = {
    id: "1",
    title: "Team Meeting",
    startTime: new Date("2025-02-15T15:00:00Z").toISOString(),
  };

  return {
    props: { event },
  };
};

export default function EventPage({ event }: EventPageProps) {
  return (
    <div>
      <h1>{event.title}</h1>
      <EventDate timestamp={event.startTime} />
    </div>
  );
}

ISO文字列は正確な時間の瞬間を保持するため、クライアント側のフォーマットで任意のタイムゾーンに正確に変換できます。