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文字列を保持し、表示されるテキストはユーザーフレンドリーな現地時刻を示します。