Files
2026-05-27 23:47:53 +01:00

4.1 KiB

useRelativeTime

(no submodules)

Purpose

useRelativeTime(deltaMs, options?) formats a millisecond delta as a localised relative-time string ("in 5 minutes", "3 hours ago", "il y a 2 jours"). Wraps Intl.RelativeTimeFormat directly with a per-(locale, style, numeric) cache so the formatter constructor is paid once per process per option combination. Memoised on (resolvedLocale, deltaMs, options-hash) so a no-op re-render does not reach into the cache or the formatter.

When the unit option is omitted the hook auto-selects the largest unit whose absolute value is ≥ 1 (per the universal-core formatRelativeTime contract). Pass unit explicitly when a fixed granularity matters — e.g. always render activity timestamps in minutes within the first hour for consistency with a refresh interval.

Features

  • Auto-selects unit (seconds → years) when unit is omitted.
  • Forwards style ('long' | 'short' | 'narrow') and numeric ('always' | 'auto').
  • Per-(locale, style, numeric) cache of Intl.RelativeTimeFormat instances.
  • Memoised on (locale, deltaMs, options-hash) — no re-format on identical inputs.
  • Production no-Provider fallback uses 'en-GB' so the hook always returns a real string.
  • Re-renders on languageChanged via the inherited useLocale subscription.
  • clearUseRelativeTimeCache() exposed for tests / hot-reload scenarios.

Usage: Basic Example

import { useRelativeTime } from "@suaveplan/react-i18n";

export function PostedAt(props: { readonly postedAt: Date }) {
    const delta = props.postedAt.getTime() - Date.now();
    const formatted = useRelativeTime(delta);
    return <time dateTime={props.postedAt.toISOString()}>{formatted}</time>;
}

Usage: Advanced Example

For activity feeds that update once per second, pin the unit to 'seconds' for the first minute and 'minutes' thereafter. The pattern below shows a row that picks the unit explicitly to avoid stuttering between auto-selected units near boundary thresholds.

import { useRelativeTime } from "@suaveplan/react-i18n";

export function LiveTimestamp(props: { readonly when: Date }) {
    const delta = props.when.getTime() - Date.now();
    const abs = Math.abs(delta);
    const unit = abs < 60_000 ? "seconds" : "minutes";
    const formatted = useRelativeTime(delta, {
        style: "short",
        numeric: "always",
        unit,
    });
    return <span data-live="true">{formatted}</span>;
}

API Reference

useRelativeTime(deltaMs: number, options?: UseRelativeTimeOptions): string

  • Parameters:
    • deltaMs — delta in milliseconds. Positive = future, negative = past, zero = now.
    • options? — style, numeric, unit. When unit is omitted, the hook auto-selects.
  • Returns: the formatted relative-time string.
  • Throws: ReactI18nError(REACT_I18N.NO_PROVIDER) outside a <TranslationProvider> in dev mode (inherited from useLocale → useI18n).

clearUseRelativeTimeCache(): void

Test-only escape hatch to reset the process-level Intl.RelativeTimeFormat cache.

Implementation Notes

  • The auto-select thresholds match the universal-core formatRelativeTime table: 60 s → minutes, 60 min → hours, 24 h → days, 7 d → weeks, 30.4375 d → months (Gregorian average), 365.25 d → years (Julian average). This keeps React-side and server-side renders stable across the boundary.
  • The unit conversion rounds via Math.round on the way down. Intentionally — a 30.5-second delta rounds to "in 31 seconds" rather than truncating to "in 30 seconds" which would feel wrong to the user staring at a one-second-resolution clock.
  • The cache key is (locale, style, numeric) — unit does NOT enter the cache because Intl.RelativeTimeFormat.format(value, unit) accepts unit per call. One formatter handles all units for the same (locale, style, numeric) combination.
  • The default locale 'en-GB' on the no-Provider fallback matches the broader package convention. This keeps the hook from throwing in production while still producing meaningful output.