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

Отображение времени в часовом поясе пользователя

Проблема

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

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

Решение

Сохраняйте все временные метки в универсальном формате, таком как строки ISO 8601 с указателями UTC или временные метки Unix. При отображении этих временных меток пользователям преобразуйте их в объекты Date в JavaScript и форматируйте их с использованием 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"
        />
        {" в "}
        <FormattedTime value={startTime} />
        {" до "}
        <FormattedTime value={endTime} />
      </p>
    </div>
  );
}

Компоненты FormattedDate и FormattedTime автоматически используют определённый браузером часовой пояс для отображения корректного локального времени. Пользователь в Нью-Йорке увидит "15 марта 2024 года в 15:00", а пользователь в Токио — "16 марта 2024 года в 5: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} в {formattedTime}
      </time>
    </div>
  );
}

Методы formatDate и formatTime возвращают строки, отформатированные в соответствии с локалью и часовым поясом пользователя. Атрибут dateTime сохраняет оригинальную строку ISO для машинной читаемости, в то время как отображаемый текст показывает удобное для пользователя локальное время.