Как работать с временными зонами в Next.js (Pages Router) v16

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

Проблема

Когда приложения отображают время без учета местоположения пользователя, возникает путаница. Встреча, запланированная на "15:00", означает разные моменты времени в зависимости от того, находится ли пользователь в Нью-Йорке, Лондоне или Токио. Если сервер отображает время в UTC или в своем часовом поясе, пользователи из других регионов видят некорректное местное время, что приводит к пропущенным встречам и ошибкам в расписании. Один и тот же временной штамп должен интерпретироваться по-разному для каждого пользователя в зависимости от его часового пояса.

Решение

Сохраняйте временные метки в универсальном формате, таком как ISO 8601 или Unix timestamps, а затем форматируйте их для отображения в местном часовом поясе пользователя на клиенте. Определите часовой пояс пользователя с помощью API интернационализации браузера и передайте его в функции форматирования react-intl. Это гарантирует, что каждый пользователь видит время, скорректированное под его часовой пояс, устраняя двусмысленность относительно времени событий.

Шаги

1. Определите часовой пояс пользователя на клиенте

Метод браузера Intl.DateTimeFormat().resolvedOptions().timeZone возвращает идентификатор часового пояса пользователя в формате IANA, например, America/New_York или Europe/London. Создайте пользовательский хук для доступа к этому значению.

import { useState, useEffect } from "react";

export function useUserTimeZone() {
  const [timeZone, setTimeZone] = useState<string>("UTC");

  useEffect(() => {
    const detected = Intl.DateTimeFormat().resolvedOptions().timeZone;
    setTimeZone(detected);
  }, []);

  return timeZone;
}

Этот хук выполняется только на клиенте, избегая несоответствий гидратации, и по умолчанию использует UTC до определения часового пояса.

2. Форматируйте даты с учетом часового пояса пользователя с помощью FormattedDate

Передайте опцию timeZone в методы форматирования react-intl, чтобы управлять часовым поясом, используемым для отображения. Используйте компонент <FormattedDate> с определенным часовым поясом.

import { FormattedDate } from "react-intl";
import { useUserTimeZone } from "../hooks/useUserTimeZone";

interface EventDateProps {
  timestamp: string;
}

export function EventDate({ timestamp }: EventDateProps) {
  const userTimeZone = useUserTimeZone();

  return (
    <FormattedDate
      value={new Date(timestamp)}
      timeZone={userTimeZone}
      year="numeric"
      month="long"
      day="numeric"
      hour="numeric"
      minute="2-digit"
      timeZoneName="short"
    />
  );
}

Свойство timeZone гарантирует, что дата будет отформатирована в местном часовом поясе пользователя, а не в часовом поясе сервера или UTC.

3. Форматируйте время императивно с помощью useIntl

В ситуациях, когда вам нужна отформатированная строка, а не компонент, используйте хук useIntl для доступа к императивному API форматирования.

import { useIntl } from "react-intl";
import { useUserTimeZone } from "../hooks/useUserTimeZone";

interface MeetingTimeProps {
  startTime: string;
}

export function MeetingTime({ startTime }: MeetingTimeProps) {
  const intl = useIntl();
  const userTimeZone = useUserTimeZone();

  const formattedTime = intl.formatDate(new Date(startTime), {
    timeZone: userTimeZone,
    hour: "numeric",
    minute: "2-digit",
    timeZoneName: "short",
  });

  return <span title={startTime}>{formattedTime}</span>;
}

Метод formatDate принимает вторым аргументом объект с DateTimeFormatOptions, включая timeZone, что позволяет полностью контролировать отображение временной метки.

4. Передавайте серверные временные метки в виде строк ISO

В getServerSideProps получайте данные во время запроса и передавайте их в компонент страницы в виде пропсов. Сериализуйте даты в строки формата ISO 8601, чтобы сохранить информацию о временной зоне.

import { GetServerSideProps } from "next";

interface Event {
  id: string;
  title: string;
  startTime: string;
}

interface EventPageProps {
  event: Event;
}

export const getServerSideProps: GetServerSideProps<
  EventPageProps
> = async () => {
  const event = {
    id: "1",
    title: "Командная встреча",
    startTime: new Date("2025-02-15T15:00:00Z").toISOString(),
  };

  return {
    props: { event },
  };
};

export default function EventPage({ event }: EventPageProps) {
  return (
    <div>
      <h1>{event.title}</h1>
      <EventDate timestamp={event.startTime} />
    </div>
  );
}

Строки формата ISO сохраняют точный момент времени, что позволяет клиентскому форматированию корректно преобразовывать их в любую временную зону.