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

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

問題

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

解決策

タイムスタンプをISO 8601やUnixタイムスタンプなどの普遍的な形式で保存し、クライアント側でユーザーのローカルタイムゾーンに合わせて表示用にフォーマットします。ブラウザのInternationalization 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では、リクエスト時にデータを取得し、propsとしてページコンポーネントに渡します。タイムゾーン情報を保持するために、日付を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文字列は正確な時点を保持するため、クライアント側のフォーマットで任意のタイムゾーンに正確に変換できます。