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>
  );
}

FormattedDateFormattedTimeコンポーネントは、ブラウザで検出されたタイムゾーンを自動的に使用して、正確なローカル時間を表示します。同じ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文字列を保持し、表示されるテキストはユーザーフレンドリーなローカル時間を示します。