30% offEnding soon
useTimeAgoLoading saved progress…

useTimeAgo

A relative-time formatter turns an absolute timestamp into a human phrase measured from the present moment — 30 seconds ago, in 2 hours, yesterday — and keeps that phrase current as real time passes. You'll build useTimeAgo, a React hook that formats a Date (or a millisecond timestamp) with Intl.RelativeTimeFormat and then re-renders itself on a self-scheduled timer, so a label that reads "5 seconds ago" quietly becomes "1 minute ago" without the parent lifting a finger. It is the reactive timestamp you see under every post, comment, and commit.

Signature

function useTimeAgo(
  date: Date | number,            // the instant to describe
  options?: {
    now?: () => number;           // clock source; defaults to Date.now
    locale?: string;              // BCP-47 tag passed to Intl; defaults to the runtime locale
    numeric?: 'auto' | 'always';  // "yesterday" vs "1 day ago"; defaults to 'auto'
  }
): string;                        // e.g. "3 minutes ago"

Examples

Assume the clock is fixed at 2026-08-03T12:00:00Z:

useTimeAgo(new Date('2026-08-03T11:59:30Z')); // "30 seconds ago"
useTimeAgo(new Date('2026-08-03T11:58:30Z')); // "1 minute ago"  (90s rounds down)
useTimeAgo(new Date('2026-08-03T14:00:00Z')); // "in 2 hours"    (a future date)
useTimeAgo(new Date('2026-08-02T11:00:00Z')); // "yesterday"     (numeric: 'auto')

The label also updates on its own. Mounted against a fresh timestamp it re-renders roughly once a second; once the difference is minutes wide it slows to about once a minute:

const label = useTimeAgo(comment.createdAt);
// renders "5 seconds ago", then "1 minute ago" a minute later — on its own timer.

Notes

  • Inject the clock. Read the current time through options.now (default Date.now) so output is deterministic and unit-testable. Don't reach for Date.now() directly inside the hook.
  • Pick the largest unit. Choose the biggest unit whose absolute value is at least 1: under 60s → seconds, under 60m → minutes, then hours, days, weeks, months, years. The sign of the difference decides "ago" versus "in".
  • Adapt the refresh rate. A seconds-old label can change every second; a five-year-old one barely changes at all. The timer's delay must scale with the magnitude — don't fire every second forever, and don't let a fresh label go stale.
  • Clean up on unmount. Clear the pending timer, or you leak a callback that wakes up to update a component that is no longer there.
  • Prefer Intl over hardcoded strings. Intl.RelativeTimeFormat gives locale-correct output for free; with numeric: 'auto' a sub-second difference reads "now" — the built-in "just now".