Пользовательские хуки позволяют инкапсулировать логику обновления относительного времени и переиспользовать её в компонентах.
import { useState, useEffect } from 'react';
import { format } from 'timeago.js';
export function useTimeAgo(
date: Date | string | number,
locale = 'ru'
): string {
const [value, setValue] = useState(() => format(date, locale));
useEffect(() => {
setValue(format(date, locale));
const id = setInterval(() => {
setValue(format(date, locale));
}, 30000);
return () => clearInterval(id);
}, [date, locale]);
return value;
}
Использование:
function PostCard({ post }) {
const timeLabel = useTimeAgo(post.createdAt, 'ru');
return <time>{timeLabel}</time>;
}
import { useState, useEffect } from 'react';
import { format } from 'timeago.js';
function getInterval(date: Date | string | number): number {
const diff = Math.abs(Date.now() - new Date(date).getTime());
if (diff < 60000) return 10000; // секунды → каждые 10с
if (diff < 3600000) return 60000; // минуты → каждую минуту
if (diff < 86400000) return 1800000; // часы → каждые 30 мин
return 86400000; // дни → раз в день
}
export function useAdaptiveTimeAgo(date: Date | string | number, locale = 'ru'): string {
const [value, setValue] = useState(() => format(date, locale));
useEffect(() => {
setValue(format(date, locale));
let id: ReturnType<typeof setTimeout>;
function tick() {
setValue(format(date, locale));
id = setTimeout(tick, getInterval(date));
}
id = setTimeout(tick, getInterval(date));
return () => clearTimeout(id);
}, [date, locale]);
return value;
}
import { useState, useEffect } from 'react';
import { format } from 'timeago.js';
export function useTimeAgoList(
dates: (Date | string | number)[],
locale = 'ru'
): string[] {
const [values, setValues] = useState(() => dates.map(d => format(d, locale)));
useEffect(() => {
setValues(dates.map(d => format(d, locale)));
const id = setInterval(() => {
setValues(dates.map(d => format(d, locale)));
}, 60000);
return () => clearInterval(id);
}, [dates, locale]);
return values;
}
import { useState, useEffect } from 'react';
import { format } from 'timeago.js';
interface TimeAgoMeta {
label: string;
isPast: boolean;
ageMs: number;
ageDays: number;
}
export function useTimeAgoMeta(date: string | Date | number, locale = 'ru'): TimeAgoMeta {
const getMeta = (): TimeAgoMeta => {
const d = new Date(date);
const diff = Date.now() - d.getTime();
return {
label: format(d, locale),
isPast: diff >= 0,
ageMs: Math.abs(diff),
ageDays: Math.abs(diff) / 86400000,
};
};
const [meta, setMeta] = useState(getMeta);
useEffect(() => {
setMeta(getMeta());
const id = setInterval(() => setMeta(getMeta()), 60000);
return () => clearInterval(id);
}, [date, locale]);
return meta;
}
import { useState, useEffect, useRef } from 'react';
import { format } from 'timeago.js';
export function usePausableTimeAgo(date: string, locale = 'ru') {
const [label, setLabel] = useState(() => format(date, locale));
const [paused, setPaused] = useState(false);
useEffect(() => {
if (paused) return;
const id = setInterval(() => {
setLabel(format(date, locale));
}, 30000);
return () => clearInterval(id);
}, [date, locale, paused]);
return { label, paused, pause: () => setPaused(true), resume: () => setPaused(false) };
}
import { useContext } from 'react';
import { LocaleContext } from '../contexts/LocaleContext';
import { useTimeAgo } from './useTimeAgo';
export function useContextTimeAgo(date: string): string {
const locale = useContext(LocaleContext);
return useTimeAgo(date, locale);
}
При SSR начальное значение может отличаться от клиентского (гидратация):
import { useState, useEffect } from 'react';
import { format } from 'timeago.js';
export function useSSRSafeTimeAgo(date: string, locale = 'ru'): string {
const [label, setLabel] = useState('');
useEffect(() => {
// Только на клиенте
setLabel(format(date, locale));
const id = setInterval(() => setLabel(format(date, locale)), 60000);
return () => clearInterval(id);
}, [date, locale]);
return label;
}
Пустая строка на сервере предотвращает гидрационный конфликт.
import { useMemo } from 'react';
import { format } from 'timeago.js';
interface Item {
id: number;
createdAt: string;
}
interface GroupedItems {
label: string;
items: Item[];
}
export function useGroupedByDate(items: Item[], locale = 'ru'): GroupedItems[] {
return useMemo(() => {
const now = Date.now();
const DAY = 86400000;
const WEEK = DAY * 7;
const groups: Record<string, Item[]> = {
'Сегодня': [],
'Вчера': [],
'На этой неделе': [],
'Ранее': [],
};
items.forEach(item => {
const age = now - new Date(item.createdAt).getTime();
if (age < DAY) groups['Сегодня'].push(item);
else if (age < DAY * 2) groups['Вчера'].push(item);
else if (age < WEEK) groups['На этой неделе'].push(item);
else groups['Ранее'].push(item);
});
return Object.entries(groups)
.filter(([, list]) => list.length > 0)
.map(([label, items]) => ({ label, items }));
}, [items]);
}