Как работать с часовыми поясами в Next.js (Pages Router) v16

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

Проблема

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

Решение

Храните таймстампы в универсальном формате, например ISO 8601 или Unix timestamp, а для отображения форматируйте их в часовом поясе пользователя на клиенте. Определяйте часовой пояс пользователя через Internationalization 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 получайте данные во время запроса и передавайте их в компонент страницы через props. Сериализуйте даты в строки формата 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: "Team Meeting",
    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-строки сохраняют точный момент времени, что позволяет корректно конвертировать их в любой часовой пояс на клиенте.