How to format relative time in TanStack Start v1
Format timestamps as '2 days ago' phrases
Problem
Displaying timestamps as relative time phrases like "2 days ago" or "in 3 hours" makes temporal information more intuitive for users. However, these phrases follow complex grammatical rules that vary by language. English places "ago" after past quantities and "in" before future ones, but other languages may use different word orders, inflect time units, or employ entirely different grammatical structures. Manually constructing these phrases with string concatenation produces incorrect output in every language except the one you hardcoded.
The underlying formatting API requires a numeric value and a time unit, but timestamps arrive as Date objects or millisecond values. Converting a timestamp into the appropriate unit and value requires calculating the difference from the current moment and selecting the most natural unit to express that difference.
Solution
Calculate the time difference between a timestamp and the current moment, select the most appropriate unit for that difference, then format the result using locale-specific relative time formatting. This transforms raw timestamps into grammatically correct phrases that adapt to the user's language.
Use react-intl's relative time formatting with a helper function that determines the best unit. The helper compares the time difference against thresholds for each unit and returns both the numeric value and the unit name. The formatter then applies the correct grammatical rules for the active locale.
Steps
1. Create a unit selection helper
Build a function that calculates the time difference and selects the most natural unit to express it.
type RelativeTimeUnit =
| "second"
| "minute"
| "hour"
| "day"
| "week"
| "month"
| "year";
interface RelativeTimeValue {
value: number;
unit: RelativeTimeUnit;
}
export function selectRelativeTimeUnit(
timestamp: Date | number,
baseTime: Date | number = Date.now(),
): RelativeTimeValue {
const date = typeof timestamp === "number" ? timestamp : timestamp.getTime();
const base = typeof baseTime === "number" ? baseTime : baseTime.getTime();
const diffMs = date - base;
const absDiff = Math.abs(diffMs);
const minute = 60 * 1000;
const hour = 60 * minute;
const day = 24 * hour;
const week = 7 * day;
const month = 30 * day;
const year = 365 * day;
if (absDiff < minute) {
return { value: Math.round(diffMs / 1000), unit: "second" };
}
if (absDiff < hour) {
return { value: Math.round(diffMs / minute), unit: "minute" };
}
if (absDiff < day) {
return { value: Math.round(diffMs / hour), unit: "hour" };
}
if (absDiff < week) {
return { value: Math.round(diffMs / day), unit: "day" };
}
if (absDiff < month) {
return { value: Math.round(diffMs / week), unit: "week" };
}
if (absDiff < year) {
return { value: Math.round(diffMs / month), unit: "month" };
}
return { value: Math.round(diffMs / year), unit: "year" };
}
This function converts a timestamp into a relative value and unit by comparing the time difference against fixed thresholds. It returns negative values for past times and positive values for future times, which the formatter interprets correctly.
2. Create a relative time component
Build a component that combines the unit selection helper with react-intl's formatting.
import { FormattedRelativeTime } from "react-intl";
import { selectRelativeTimeUnit } from "./selectRelativeTimeUnit";
interface RelativeTimeProps {
date: Date | number;
numeric?: "always" | "auto";
style?: "long" | "short" | "narrow";
}
export function RelativeTime({
date,
numeric = "auto",
style = "long",
}: RelativeTimeProps) {
const { value, unit } = selectRelativeTimeUnit(date);
return (
<FormattedRelativeTime
value={value}
unit={unit}
numeric={numeric}
style={style}
/>
);
}
This component accepts a timestamp and formatting options, calculates the relative value and unit, then delegates to react-intl's component for locale-aware rendering. The numeric="auto" option produces phrases like "yesterday" instead of "1 day ago" when appropriate.
3. Use the component in your routes
Import and render the component wherever you need to display relative timestamps.
import { createFileRoute } from "@tanstack/react-router";
import { RelativeTime } from "../components/RelativeTime";
export const Route = createFileRoute("/posts/$postId")({
component: PostPage,
});
function PostPage() {
const post = {
title: "Understanding Relative Time",
publishedAt: new Date("2024-11-15T10:30:00Z"),
updatedAt: new Date(Date.now() - 2 * 60 * 60 * 1000),
};
return (
<article>
<h1>{post.title}</h1>
<p>
Published <RelativeTime date={post.publishedAt} />
</p>
<p>
Updated <RelativeTime date={post.updatedAt} style="short" />
</p>
</article>
);
}
The component works in both server-rendered and client-rendered contexts. On the server it produces the initial relative time phrase, and on the client it displays the same phrase using the user's locale from the IntlProvider.
4. Add an imperative formatting option
For cases where you need the formatted string directly, create a hook-based helper.
import { useIntl } from "react-intl";
import { selectRelativeTimeUnit } from "./selectRelativeTimeUnit";
export function useRelativeTime() {
const intl = useIntl();
return (
date: Date | number,
options?: {
numeric?: "always" | "auto";
style?: "long" | "short" | "narrow";
},
) => {
const { value, unit } = selectRelativeTimeUnit(date);
return intl.formatRelativeTime(value, unit, options);
};
}
This hook returns a function that formats timestamps imperatively, useful for setting text attributes or computing values outside JSX.
5. Use the hook for non-component contexts
Call the hook in components where you need formatted strings for attributes or logic.
import { createFileRoute } from "@tanstack/react-router";
import { useRelativeTime } from "../hooks/useRelativeTime";
export const Route = createFileRoute("/events/$eventId")({
component: EventPage,
});
function EventPage() {
const formatRelativeTime = useRelativeTime();
const event = {
name: "Product Launch",
startTime: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
};
const timeUntil = formatRelativeTime(event.startTime);
return (
<div>
<h1>{event.name}</h1>
<time dateTime={event.startTime.toISOString()} title={timeUntil}>
{timeUntil}
</time>
</div>
);
}
The hook provides the same formatting logic in a functional form, allowing you to use relative time strings in attributes, computed values, or anywhere a component cannot be rendered.