如何在 React Router v7 中处理时区问题

以用户本地时区显示时间

问题

当应用程序在显示时间时未考虑用户所在位置,就会引发混淆和错误。例如,服务器可能将事件时间存储为 "2024-03-15T20:00:00Z",并直接以 UTC 值渲染为 "8:00 PM"。不同时区的用户看到相同的时间,但会将其理解为本地时间,导致错过约会和排班冲突。根本问题在于,同一时刻在不同地理位置有不同的表示方式,如果显示的时区不正确,时间信息就会变得无意义或具有误导性。

当用户跨地区协作时,这一问题会进一步加剧。服务器上安排的 3:00 PM 事件,在界面中会被显示为各地的 3:00 PM,实际上同一时刻在东京应为 6:00 AM,在伦敦为 2:00 PM,在纽约为 9:00 AM。

解决方案

应将所有时间戳以通用格式存储,例如带有 UTC 标识的 ISO 8601 字符串或 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 组件会自动使用浏览器检测到的时区来显示正确的本地时间。例如,纽约的用户会看到“2024 年 3 月 15 日 15:00”,而东京的用户会看到同一 UTC 时间戳对应的“2024 年 3 月 16 日 05:00”。

3. 针对动态格式化需求使用命令式 API

当您需要在属性、计算值或非 JSX 场景下格式化时间时,请使用 useIntl hook。

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

formatDateformatTime 方法会根据用户的语言环境和时区返回格式化后的字符串。dateTime 属性保留原始 ISO 字符串以便机器读取,而显示文本则呈现用户友好的本地时间。