How to handle timezones in React Router v7
Show times in user's local timezone
Problem
When applications display times without considering the user's location, confusion and errors follow. A server might store an event time as "2024-03-15T20:00:00Z" and render "8:00 PM" directly from that UTC value. Users in different time zones see the same clock time but interpret it as local to them, leading to missed appointments and scheduling conflicts. The fundamental issue is that a single moment in time has different representations depending on geographic location, and displaying times in the wrong zone makes them meaningless or misleading.
This problem compounds when users collaborate across regions. An event scheduled for 3:00 PM on the server becomes 3:00 PM everywhere in the UI, even though it should appear as 6:00 AM in Tokyo, 2:00 PM in London, and 9:00 AM in New York for the same actual moment.
Solution
Store all timestamps in a universal format such as ISO 8601 strings with UTC indicators or Unix timestamps. When displaying these times to users, convert them to JavaScript Date objects and format them using internationalization APIs that respect the browser's timezone. Modern browsers automatically detect the user's local timezone, and formatting libraries leverage this to convert UTC timestamps into the correct local representation without manual offset calculations.
This approach separates storage from presentation. The server sends one canonical timestamp, and each client renders it according to their own timezone, ensuring everyone sees the correct local time for the same global moment.
Steps
1. Send timestamps in ISO 8601 format from your data source
Ensure your API or data loader returns timestamps as ISO 8601 strings with UTC indicators or as Unix timestamps in milliseconds.
export async function loader() {
const event = await fetchEvent();
return {
title: event.title,
startTime: "2024-03-15T20:00:00Z",
endTime: "2024-03-15T22:00:00Z",
};
}
The Z suffix indicates UTC time. When this string is parsed into a JavaScript Date object, the browser will store it as a UTC timestamp internally.
2. Create a component that formats times using react-intl
Import formatting components from react-intl and pass Date objects created from your ISO strings.
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>
);
}
The FormattedDate and FormattedTime components automatically use the browser's detected timezone to display the correct local time. A user in New York sees "March 15, 2024 at 3:00 PM" while a user in Tokyo sees "March 16, 2024 at 5:00 AM" for the same UTC timestamp.
3. Use the imperative API for dynamic formatting needs
When you need formatted times in attributes, computed values, or non-JSX contexts, use the useIntl hook.
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>
);
}
The formatDate and formatTime methods return strings formatted according to the user's locale and timezone. The dateTime attribute preserves the original ISO string for machine readability while the displayed text shows the user-friendly local time.