React Router v7でタイムゾーンを処理する方法
ユーザーのローカルタイムゾーンで時間を表示する
問題
アプリケーションがユーザーの所在地を考慮せずに時刻を表示すると、混乱やエラーが発生します。サーバーはイベント時刻を「2024-03-15T20:00:00Z」として保存し、そのUTC値から直接「8:00 PM」とレンダリングすることがあります。異なるタイムゾーンのユーザーは同じ時刻を見ますが、それぞれ自分のローカル時間として解釈するため、予定の見落としやスケジュールの衝突が発生します。根本的な問題は、一つの時点が地理的位置によって異なる表現を持つことであり、間違ったゾーンで時刻を表示すると意味がなくなったり誤解を招いたりします。
この問題は、ユーザーが地域を越えて協力する場合に複雑化します。サーバー上で午後3時に予定されたイベントは、UIでは実際には東京では午前6時、ロンドンでは午後2時、ニューヨークでは午前9時と表示されるべきであるにもかかわらず、どこでも午後3時として表示されます。
解決策
すべてのタイムスタンプをISO 8601形式の文字列(UTC指示子付き)やUnixタイムスタンプなどの普遍的な形式で保存します。これらの時刻をユーザーに表示する際は、JavaScriptのDateオブジェクトに変換し、ブラウザのタイムゾーンを尊重する国際化APIを使用してフォーマットします。最新のブラウザはユーザーのローカルタイムゾーンを自動的に検出し、フォーマットライブラリはこれを活用してUTCタイムスタンプを手動でオフセット計算することなく、正確なローカル表現に変換します。
このアプローチは保存と表示を分離します。サーバーは一つの標準的なタイムスタンプを送信し、各クライアントは自身のタイムゾーンに従ってレンダリングすることで、誰もが同じグローバルな瞬間に対して正確なローカル時間を見ることができます。
手順
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コンポーネントは、ブラウザで検出されたタイムゾーンを自動的に使用して、正確なローカル時間を表示します。同じUTCタイムスタンプに対して、ニューヨークのユーザーには「March 15, 2024 at 3:00 PM」と表示され、東京のユーザーには「March 16, 2024 at 5:00 AM」と表示されます。
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文字列を保持し、表示されるテキストはユーザーフレンドリーなローカル時間を示します。