How to format relative time in React Router v7

Format timestamps as '2 days ago' phrases

Problem

Displaying timestamps as relative time phrases like "2 days ago" or "in 3 hours" makes content feel immediate and contextual. However, these phrases follow complex grammatical rules that vary dramatically across languages. English places "ago" after past quantities and "in" before future ones, but other languages may inflect the time unit itself, reorder words, or use entirely different grammatical structures. Manually constructing these phrases with string concatenation produces incorrect output in every language except the one you hardcoded, breaking the user experience for international audiences.

Solution

Use locale-aware relative time formatting to convert timestamp differences into grammatically correct phrases. Calculate the time difference between a given timestamp and the current moment, then format that difference using the user's locale rules. This ensures that past and future time expressions follow the correct grammar, word order, and inflection patterns for each language without manual string manipulation.

Steps

1. Create a helper to calculate relative time values

The FormattedRelativeTime component requires a numeric value and a unit prop. Build a helper function that computes the time difference and selects an appropriate unit.

export function getRelativeTimeValue(date: Date | number) {
  const now = Date.now();
  const timestamp = typeof date === "number" ? date : date.getTime();
  const diffInSeconds = Math.round((timestamp - now) / 1000);

  const minute = 60;
  const hour = minute * 60;
  const day = hour * 24;
  const week = day * 7;
  const month = day * 30;
  const year = day * 365;

  const absDiff = Math.abs(diffInSeconds);

  if (absDiff < minute) {
    return { value: diffInSeconds, unit: "second" as const };
  } else if (absDiff < hour) {
    return {
      value: Math.round(diffInSeconds / minute),
      unit: "minute" as const,
    };
  } else if (absDiff < day) {
    return { value: Math.round(diffInSeconds / hour), unit: "hour" as const };
  } else if (absDiff < week) {
    return { value: Math.round(diffInSeconds / day), unit: "day" as const };
  } else if (absDiff < month) {
    return { value: Math.round(diffInSeconds / week), unit: "week" as const };
  } else if (absDiff < year) {
    return { value: Math.round(diffInSeconds / month), unit: "month" as const };
  } else {
    return { value: Math.round(diffInSeconds / year), unit: "year" as const };
  }
}

This function converts a timestamp into a signed numeric value and the most appropriate time unit based on the magnitude of the difference.

2. Create a relative time component using FormattedRelativeTime

Use the FormattedRelativeTime component from react-intl, which renders formatted relative time and can optionally update at intervals.

import { FormattedRelativeTime } from "react-intl";
import { getRelativeTimeValue } from "./getRelativeTimeValue";

interface RelativeTimeProps {
  date: Date | number;
  updateIntervalInSeconds?: number;
}

export function RelativeTime({
  date,
  updateIntervalInSeconds,
}: RelativeTimeProps) {
  const { value, unit } = getRelativeTimeValue(date);

  return (
    <FormattedRelativeTime
      value={value}
      unit={unit}
      numeric="auto"
      updateIntervalInSeconds={updateIntervalInSeconds}
    />
  );
}

The numeric="auto" option allows the formatter to use phrases like "yesterday" instead of "1 day ago" when appropriate. The optional updateIntervalInSeconds prop controls how often the component re-renders to keep the relative time current.

3. Use the component in a React Router route

Render the relative time component in any route component where you need to display timestamps.

import { RelativeTime } from "./RelativeTime";

interface Post {
  id: string;
  title: string;
  content: string;
  createdAt: number;
}

export function PostDetail({ post }: { post: Post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <time dateTime={new Date(post.createdAt).toISOString()}>
        <RelativeTime date={post.createdAt} />
      </time>
      <p>{post.content}</p>
    </article>
  );
}

The component automatically formats the timestamp according to the user's locale, producing phrases like "2 days ago" in English, "il y a 2 jours" in French, or "hace 2 días" in Spanish.

4. Format relative time imperatively with useIntl

For cases where you need the formatted string directly (such as setting element attributes), use the formatRelativeTime function from the useIntl hook.

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

interface CommentProps {
  author: string;
  text: string;
  timestamp: number;
}

export function Comment({ author, text, timestamp }: CommentProps) {
  const intl = useIntl();
  const { value, unit } = getRelativeTimeValue(timestamp);
  const relativeTime = intl.formatRelativeTime(value, unit, {
    numeric: "auto",
  });

  return (
    <div aria-label={`Comment by ${author}, posted ${relativeTime}`}>
      <strong>{author}</strong>
      <p>{text}</p>
    </div>
  );
}

This approach gives you a formatted string that you can use in attributes, concatenate with other text, or pass to non-React APIs while maintaining full locale support.