TanStack Start v1でタイムゾーンを扱う方法

ユーザーのローカルタイムゾーンで時間を表示する

問題

アプリケーションが時刻を表示する際、それは視聴者の場所によって異なる表示になる単一の瞬間を表します。サーバーが自身のタイムゾーンまたはUTCで「午後8時」をレンダリングすると、異なるタイムゾーンのユーザーは不正確なローカル時間を見ることになり、イベントが実際に発生する時間について混乱が生じます。ユーザーは時刻が自動的に自分のローカルタイムゾーンを反映することを期待していますが、適切な処理がなければ、サーバーがレンダリング中に使用したタイムゾーンの時刻が表示されます。このミスマッチにより、ユーザーは予定を逃したり、スケジュールを誤解したり、アプリケーションの信頼性に対する信頼を失ったりする原因となります。

解決策

日付フォーマットメソッドにtimeZoneオプションを渡して、日付の解釈と表示方法を制御します。formatDate関数はタイムゾーン設定を含むIntl.DateTimeFormatOptionsを受け入れます。タイムスタンプをISO 8601文字列やUnixタイムスタンプなどの普遍的な形式で保存し、ブラウザのタイムゾーンが自動的に利用可能なクライアント側でフォーマットします。Intl.DateTimeFormat APIはtimeZoneオプションが指定されていない場合、ユーザーのデフォルトタイムゾーンを使用し、各ユーザーの場所に合わせて手動のタイムゾーン検出なしに時刻が正しく表示されるようにします。

ステップ

1. タイムゾーンを考慮した日付フォーマットコンポーネントを作成する

useIntlフックを使用してintlオブジェクトとそのformatDateメソッドにアクセスします。このコンポーネントはISOタイムスタンプを受け取り、ユーザーのローカルタイムゾーンに合わせてフォーマットします。

import { useIntl } from "react-intl";

interface LocalTimeProps {
  timestamp: string;
  showDate?: boolean;
  showTime?: boolean;
}

export function LocalTime({
  timestamp,
  showDate = true,
  showTime = true,
}: LocalTimeProps) {
  const intl = useIntl();
  const date = new Date(timestamp);

  const formatted = intl.formatDate(date, {
    ...(showDate && {
      year: "numeric",
      month: "short",
      day: "numeric",
    }),
    ...(showTime && {
      hour: "numeric",
      minute: "2-digit",
      timeZoneName: "short",
    }),
  });

  return <time dateTime={timestamp}>{formatted}</time>;
}

FormattedDateコンポーネントとformatDateメソッドはDateTimeFormatOptionsを持つIntl.DateTimeFormat APIを使用します。明示的なtimeZoneが提供されていない場合、ブラウザは自動的にユーザーのタイムゾーンを適用し、UTCタイムスタンプをローカル時間に変換します。

2. イベント時間を表示するためのコンポーネントを使用する

タイムスタンプを表示する必要がある場所ではどこでも、LocalTimeコンポーネントをインポートして使用してください。このコンポーネントは、各ユーザーのブラウザ設定に基づいてタイムゾーン変換を自動的に処理します。

import { createFileRoute } from "@tanstack/react-router";
import { LocalTime } from "~/components/LocalTime";

export const Route = createFileRoute("/events/$eventId")({
  component: EventDetail,
});

function EventDetail() {
  const event = Route.useLoaderData();

  return (
    <div>
      <h1>{event.title}</h1>
      <p>
        開始時間: <LocalTime timestamp={event.startTime} />
      </p>
      <p>
        終了時間: <LocalTime timestamp={event.endTime} />
      </p>
    </div>
  );
}

各ユーザーは、イベント時間が自分のローカルタイムゾーンに変換されて表示されます。サーバーからのISOタイムスタンプは、ユーザーのロケールとタイムゾーン設定に従って解析およびフォーマットされます。

3. 明示的なタイムゾーン表示で時間をフォーマットする

ユーザーが時間がどのタイムゾーンを表しているかを知る必要がある場合は、フォーマットオプションにタイムゾーン名を含めてください。

import { useIntl } from "react-intl";

interface ExplicitTimezoneProps {
  timestamp: string;
  timezone?: string;
}

export function ExplicitTimezone({
  timestamp,
  timezone,
}: ExplicitTimezoneProps) {
  const intl = useIntl();
  const date = new Date(timestamp);

  const formatted = intl.formatDate(date, {
    year: "numeric",
    month: "short",
    day: "numeric",
    hour: "numeric",
    minute: "2-digit",
    timeZone: timezone,
    timeZoneName: "long",
  });

  return <time dateTime={timestamp}>{formatted}</time>;
}

timeZoneオプションを指定すると、フォーマッタは特定のタイムゾーンで時間を表示するよう強制されます。timezoneが未定義の場合、ブラウザはユーザーのローカルタイムゾーンを使用します。これは、ユーザーがどこにいるかに関係なく、特定のタイムゾーンで時間を表示するのに役立ちます。

4. 相対時間表示のためのヘルパーを作成する

最近のイベントについては、絶対時間をツールチップに保持しながら、「2時間前」のような相対時間を表示します。

import { useIntl } from "react-intl";

interface RelativeTimeProps {
  timestamp: string;
}

export function RelativeTime({ timestamp }: RelativeTimeProps) {
  const intl = useIntl();
  const date = new Date(timestamp);
  const now = Date.now();
  const diffInSeconds = Math.floor((now - date.getTime()) / 1000);

  const absoluteTime = intl.formatDate(date, {
    year: "numeric",
    month: "short",
    day: "numeric",
    hour: "numeric",
    minute: "2-digit",
  });

  let value: number;
  let unit: Intl.RelativeTimeFormatUnit;

  if (diffInSeconds < 60) {
    value = -diffInSeconds;
    unit = "second";
  } else if (diffInSeconds < 3600) {
    value = -Math.floor(diffInSeconds / 60);
    unit = "minute";
  } else if (diffInSeconds < 86400) {
    value = -Math.floor(diffInSeconds / 3600);
    unit = "hour";
  } else {
    value = -Math.floor(diffInSeconds / 86400);
    unit = "day";
  }

  const relativeTime = intl.formatRelativeTime(value, unit);

  return (
    <time dateTime={timestamp} title={absoluteTime}>
      {relativeTime}
    </time>
  );
}

相対時間は、人間にわかりやすい期間を表示するように表示を更新し、titleプロパティはホバーするユーザーのために正確なローカル時間を保持します。両方のフォーマットは自動的にユーザーのタイムゾーンとロケールを尊重します。