Как работать с часовыми поясами в React Router v7

Показывайте время в часовом поясе пользователя

Проблема

Когда приложения показывают время без учёта местоположения пользователя, это приводит к путанице и ошибкам. Например, сервер может сохранить время события как "2024-03-15T20:00:00Z" и отобразить "20:00" напрямую из этого значения в UTC. Пользователи в разных часовых поясах видят одно и то же время на часах, но воспринимают его как локальное, что приводит к пропущенным встречам и накладкам в расписании. Главная проблема в том, что один и тот же момент времени отображается по-разному в зависимости от географического положения, и если показывать время в неправильном поясе, оно теряет смысл или вводит в заблуждение.

Эта проблема усугубляется, когда пользователи работают вместе из разных регионов. Событие, запланированное на 15:00 по серверу, становится 15:00 для всех в интерфейсе, хотя на самом деле это должно быть 6:00 в Токио, 14:00 в Лондоне и 9:00 в Нью-Йорке для одного и того же момента.

Решение

Храните все временные метки в универсальном формате, например, в виде строк ISO 8601 с указанием UTC или в виде Unix-меток времени. При отображении этих значений пользователям преобразуйте их в объекты JavaScript Date и форматируйте с помощью API интернационализации, которые учитывают часовой пояс браузера. Современные браузеры автоматически определяют локальный часовой пояс пользователя, а библиотеки форматирования используют это, чтобы преобразовать UTC-метки во время, корректное для местоположения пользователя, без ручных вычислений смещений.

Такой подход разделяет хранение и отображение данных. Сервер отправляет единую эталонную временную метку, а каждый клиент отображает её с учётом своего часового пояса, чтобы все видели правильное локальное время для одного и того же глобального события.

Шаги

1. Передавайте временные метки в формате ISO 8601 из вашего источника данных

Убедитесь, что ваш API или загрузчик данных возвращает временные метки в виде строк ISO 8601 с указанием UTC или как 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. Когда эта строка преобразуется в объект Date в JavaScript, браузер будет хранить её как временную метку в UTC.

2. Создайте компонент для форматирования времени с помощью react-intl

Импортируйте компоненты форматирования из react-intl и передавайте объекты Date, созданные из ваших ISO-строк.

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 автоматически используют определённый браузером часовой пояс для отображения корректного локального времени. Пользователь в Нью-Йорке увидит «15 марта 2024 г., 15:00», а пользователь в Токио — «16 марта 2024 г., 05:00» для одной и той же UTC-метки времени.

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-строку для машинного чтения, а отображаемый текст показывает удобное локальное время.