TanStack Start v1でタイムゾーンを処理する方法

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

問題

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

解決策

日付のフォーマットメソッドに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>
        Starts: <LocalTime timestamp={event.startTime} />
      </p>
      <p>
        Ends: <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がundefinedの場合、ブラウザはユーザーのローカルタイムゾーンを使用します。これは、ユーザーがどこにいても特定のタイムゾーンで時刻を表示する場合に便利です。

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属性はホバーしたユーザーのために正確なローカル時刻を保持します。両方のフォーマットは、ユーザーのタイムゾーンとロケールを自動的に尊重します。