Next.js(Pages Router)v16でタイムゾーンを処理する方法
ユーザーのローカルタイムゾーンで時刻を表示する
問題
アプリケーションがユーザーの所在地を考慮せずに時刻を表示すると、混乱が生じます。「午後3時」に予定された会議は、閲覧者がニューヨーク、ロンドン、東京のいずれにいるかによって異なる時点を意味します。サーバーがUTCまたは独自のタイムゾーンで時刻をレンダリングすると、他の地域のユーザーには誤ったローカル時刻が表示され、約束の見逃しやスケジュールエラーにつながります。同じタイムスタンプは、各ユーザーのタイムゾーンに基づいて異なる解釈をする必要があります。
解決策
タイムスタンプをISO 8601やUnixタイムスタンプなどの普遍的な形式で保存し、クライアント側でユーザーのローカルタイムゾーンに合わせて表示用にフォーマットします。ブラウザのInternationalization 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では、リクエスト時にデータを取得し、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文字列は正確な時点を保持するため、クライアント側のフォーマットで任意のタイムゾーンに正確に変換できます。