Next.js(Pages Router)v16でタイムゾーンを処理する方法
ユーザーのローカルタイムゾーンで時間を表示する
問題
アプリケーションがユーザーの所在地を考慮せずに時刻を表示すると、混乱が生じます。「午後3時」に予定されている会議は、閲覧者がニューヨーク、ロンドン、東京のどこにいるかによって異なる瞬間を意味します。サーバーがUTCや自身のタイムゾーンで時刻をレンダリングすると、他の地域のユーザーは不正確なローカル時間を見ることになり、予定の見落としやスケジューリングエラーにつながります。同じタイムスタンプでも、各ユーザーのタイムゾーンに基づいて異なる解釈が必要です。
解決策
タイムスタンプをISO 8601やUnixタイムスタンプなどの普遍的な形式で保存し、クライアント側でユーザーのローカルタイムゾーンに合わせて表示用にフォーマットします。ブラウザの国際化APIを使用してユーザーのタイムゾーンを検出し、それをreact-intlのフォーマット関数に渡します。これにより、すべてのユーザーが自分のタイムゾーンに調整された時刻を見ることができ、イベントがいつ発生するかについての曖昧さがなくなります。
ステップ
1. クライアント側でユーザーのタイムゾーンを検出する
ブラウザのIntl.DateTimeFormat().resolvedOptions().timeZoneは、America/New_YorkやEurope/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文字列は正確な時間の瞬間を保持するため、クライアント側のフォーマットで任意のタイムゾーンに正確に変換できます。