How to handle timezones in TanStack Start v1

Show times in user's local timezone

Problem

When applications display times, they represent a single moment that appears differently depending on the viewer's location. If a server renders "8:00 PM" in its own timezone or UTC, users in different timezones will see an incorrect local time, leading to confusion about when events actually occur. Users expect times to automatically reflect their local timezone, but without proper handling, they see times in whatever timezone the server used during rendering. This mismatch causes users to miss appointments, misunderstand schedules, and lose trust in the application's reliability.

Solution

Pass a timeZone option to date formatting methods to control how dates are interpreted and displayed. The formatDate function accepts Intl.DateTimeFormatOptions which include timezone configuration. Store timestamps in a universal format like ISO 8601 strings or Unix timestamps, then format them on the client where the browser's timezone is automatically available. The Intl.DateTimeFormat API uses the user's default timezone when no timeZone option is specified, ensuring times display correctly for each user's location without manual timezone detection.

Steps

1. Create a timezone-aware date formatting component

Use the useIntl hook to access the intl object and its formatDate method. This component accepts an ISO timestamp and formats it for the user's local timezone.

import { useIntl } from "react-intl";

interface LocalTimeProps {
  timestamp: string;
  showDate?: boolean;
  showTime?: boolean;
}

export function LocalTime({
  timestamp,
  showDate = true,
  showTime = true,
}: LocalTimeProps) {
  const intl = useIntl();
  const date = new Date(timestamp);

  const formatted = intl.formatDate(date, {
    ...(showDate && {
      year: "numeric",
      month: "short",
      day: "numeric",
    }),
    ...(showTime && {
      hour: "numeric",
      minute: "2-digit",
      timeZoneName: "short",
    }),
  });

  return <time dateTime={timestamp}>{formatted}</time>;
}

The FormattedDate component and formatDate method use Intl.DateTimeFormat APIs with DateTimeFormatOptions. The browser automatically applies the user's timezone when no explicit timeZone is provided, converting the UTC timestamp to local time.

2. Use the component to display event times

Import and use the LocalTime component wherever you need to display timestamps. The component handles timezone conversion automatically based on each user's browser settings.

import { createFileRoute } from "@tanstack/react-router";
import { LocalTime } from "~/components/LocalTime";

export const Route = createFileRoute("/events/$eventId")({
  component: EventDetail,
});

function EventDetail() {
  const event = Route.useLoaderData();

  return (
    <div>
      <h1>{event.title}</h1>
      <p>
        Starts: <LocalTime timestamp={event.startTime} />
      </p>
      <p>
        Ends: <LocalTime timestamp={event.endTime} />
      </p>
    </div>
  );
}

Each user sees the event times converted to their local timezone. The ISO timestamp from the server is parsed and formatted according to the user's locale and timezone preferences.

3. Format times with explicit timezone display

When users need to know which timezone a time represents, include the timezone name in the formatting options.

import { useIntl } from "react-intl";

interface ExplicitTimezoneProps {
  timestamp: string;
  timezone?: string;
}

export function ExplicitTimezone({
  timestamp,
  timezone,
}: ExplicitTimezoneProps) {
  const intl = useIntl();
  const date = new Date(timestamp);

  const formatted = intl.formatDate(date, {
    year: "numeric",
    month: "short",
    day: "numeric",
    hour: "numeric",
    minute: "2-digit",
    timeZone: timezone,
    timeZoneName: "long",
  });

  return <time dateTime={timestamp}>{formatted}</time>;
}

Specifying a timeZone option forces the formatter to display the time in that specific timezone. When timezone is undefined, the browser uses the user's local timezone. This is useful for displaying times in a specific timezone regardless of where the user is located.

4. Create a helper for relative time display

For recent events, show relative times like "2 hours ago" while preserving the absolute time in a tooltip.

import { useIntl } from "react-intl";

interface RelativeTimeProps {
  timestamp: string;
}

export function RelativeTime({ timestamp }: RelativeTimeProps) {
  const intl = useIntl();
  const date = new Date(timestamp);
  const now = Date.now();
  const diffInSeconds = Math.floor((now - date.getTime()) / 1000);

  const absoluteTime = intl.formatDate(date, {
    year: "numeric",
    month: "short",
    day: "numeric",
    hour: "numeric",
    minute: "2-digit",
  });

  let value: number;
  let unit: Intl.RelativeTimeFormatUnit;

  if (diffInSeconds < 60) {
    value = -diffInSeconds;
    unit = "second";
  } else if (diffInSeconds < 3600) {
    value = -Math.floor(diffInSeconds / 60);
    unit = "minute";
  } else if (diffInSeconds < 86400) {
    value = -Math.floor(diffInSeconds / 3600);
    unit = "hour";
  } else {
    value = -Math.floor(diffInSeconds / 86400);
    unit = "day";
  }

  const relativeTime = intl.formatRelativeTime(value, unit);

  return (
    <time dateTime={timestamp} title={absoluteTime}>
      {relativeTime}
    </time>
  );
}

The relative time updates the display to show human-friendly durations while the title attribute preserves the exact local time for users who hover. Both formats automatically respect the user's timezone and locale.