React Router v7でタイムゾーンを処理する方法
ユーザーのローカルタイムゾーンで時刻を表示する
問題
アプリケーションがユーザーの所在地を考慮せずに時刻を表示すると、混乱やエラーが発生します。サーバーがイベント時刻を「2024-03-15T20:00:00Z」として保存し、そのUTC値から直接「8:00 PM」とレンダリングする場合、異なるタイムゾーンのユーザーは同じ時刻を見ますが、それを自分のローカル時刻として解釈するため、予定の見逃しやスケジュールの競合が発生します。根本的な問題は、時間の1つの瞬間が地理的な場所によって異なる表現を持つことであり、間違ったゾーンで時刻を表示すると、それが無意味または誤解を招くものになることです。
この問題は、ユーザーが地域を越えて協力する場合に悪化します。サーバー上で午後3時にスケジュールされたイベントは、UI上ではどこでも午後3時になりますが、実際には同じ瞬間であっても、東京では午前6時、ロンドンでは午後2時、ニューヨークでは午前9時と表示されるべきです。
解決策
すべてのタイムスタンプを、UTC指示子付きのISO 8601文字列やUnixタイムスタンプなどの普遍的な形式で保存します。これらの時刻をユーザーに表示する際は、JavaScript Dateオブジェクトに変換し、ブラウザのタイムゾーンを尊重する国際化APIを使用してフォーマットします。最新のブラウザはユーザーのローカルタイムゾーンを自動的に検出し、フォーマットライブラリはこれを活用して、手動でオフセット計算を行うことなく、UTCタイムスタンプを正しいローカル表現に変換します。
このアプローチは、保存と表示を分離します。サーバーは1つの正規のタイムスタンプを送信し、各クライアントは自分のタイムゾーンに応じてそれをレンダリングするため、全員が同じグローバルな瞬間に対して正しいローカル時刻を見ることができます。
手順
1. データソースからISO 8601形式でタイムスタンプを送信する
APIまたはデータローダーが、UTC指示子付きのISO 8601文字列、またはミリ秒単位のUnixタイムスタンプとしてタイムスタンプを返すようにします。
export async function loader() {
const event = await fetchEvent();
return {
title: event.title,
startTime: "2024-03-15T20:00:00Z",
endTime: "2024-03-15T22:00:00Z",
};
}
Z接尾辞はUTC時刻を示します。この文字列がJavaScriptのDateオブジェクトに解析されると、ブラウザは内部的にUTCタイムスタンプとして保存します。
2. react-intlを使用して時刻をフォーマットするコンポーネントを作成する
react-intlからフォーマットコンポーネントをインポートし、ISO文字列から作成したDateオブジェクトを渡します。
import { FormattedDate, FormattedTime } from "react-intl";
import type { Route } from "./+types/event";
export default function EventDetails({ loaderData }: Route.ComponentProps) {
const startTime = new Date(loaderData.startTime);
const endTime = new Date(loaderData.endTime);
return (
<div>
<h1>{loaderData.title}</h1>
<p>
<FormattedDate
value={startTime}
year="numeric"
month="long"
day="numeric"
/>
{" at "}
<FormattedTime value={startTime} />
{" to "}
<FormattedTime value={endTime} />
</p>
</div>
);
}
FormattedDateおよびFormattedTimeコンポーネントは、ブラウザで検出されたタイムゾーンを自動的に使用して、正しい現地時刻を表示します。ニューヨークのユーザーには「2024年3月15日午後3時00分」と表示され、東京のユーザーには同じUTCタイムスタンプに対して「2024年3月16日午前5時00分」と表示されます。
3. 動的なフォーマットニーズには命令型APIを使用する
属性、計算値、またはJSX以外のコンテキストでフォーマットされた時刻が必要な場合は、useIntlフックを使用します。
import { useIntl } from "react-intl";
import type { Route } from "./+types/event";
export default function EventCard({ loaderData }: Route.ComponentProps) {
const intl = useIntl();
const startTime = new Date(loaderData.startTime);
const formattedDate = intl.formatDate(startTime, {
year: "numeric",
month: "short",
day: "numeric",
});
const formattedTime = intl.formatTime(startTime, {
hour: "numeric",
minute: "2-digit",
});
return (
<div>
<h2>{loaderData.title}</h2>
<time dateTime={loaderData.startTime}>
{formattedDate} at {formattedTime}
</time>
</div>
);
}
formatDateおよびformatTimeメソッドは、ユーザーのロケールとタイムゾーンに従ってフォーマットされた文字列を返します。dateTime属性は機械可読性のために元のISO文字列を保持し、表示されるテキストはユーザーフレンドリーな現地時刻を示します。