React-приложения требуют особого подхода к timeago.js: библиотека
работает с DOM напрямую, тогда как React управляет DOM через виртуальный
DOM. Два подхода — render с useEffect и чистый
format с состоянием — применяются в зависимости от
требований.
Самый простой вариант — форматирование в компоненте без автообновления:
import { format } from 'timeago.js';
interface Props {
date: string | Date | number;
locale?: string;
}
function TimeAgo({ date, locale = 'ru' }: Props) {
return (
<time dateTime={new Date(date).toISOString()}>
{format(date, locale)}
</time>
);
}
Минус: строка не обновляется с течением времени.
Обновление вручную через хук:
import { useState, useEffect } from 'react';
import { format } from 'timeago.js';
function TimeAgo({ date, locale = 'ru' }: { date: string; locale?: string }) {
const [label, setLabel] = useState(() => format(date, locale));
useEffect(() => {
setLabel(format(date, locale));
const id = setInterval(() => {
setLabel(format(date, locale));
}, 60000); // обновлять раз в минуту
return () => clearInterval(id);
}, [date, locale]);
return <time dateTime={date}>{label}</time>;
}
Делегировать автообновление самой библиотеке через
render:
import { useEffect, useRef } from 'react';
import { render, cancel } from 'timeago.js';
function TimeAgo({ date, locale = 'ru' }: { date: string; locale?: string }) {
const ref = useRef<HTMLTimeElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
render(el, locale);
return () => cancel(el);
}, [date, locale]);
return <time ref={ref} dateTime={date} />;
}
Плюс: автообновление управляется библиотекой. Минус: вне контроля React, риск конфликта с reconciliation.
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 getInterval = () => {
const diff = Math.abs(Date.now() - new Date(date).getTime());
if (diff < 60000) return 10000;
if (diff < 3600000) return 60000;
if (diff < 86400000) return 1800000;
return 86400000;
};
let id: ReturnType<typeof setTimeout>;
const tick = () => {
setValue(format(date, locale));
id = setTimeout(tick, getInterval());
};
id = setTimeout(tick, getInterval());
return () => clearTimeout(id);
}, [date, locale]);
return value;
}
Использование:
function PostDate({ date }: { date: string }) {
const label = useTimeAgo(date);
return <time dateTime={date}>{label}</time>;
}
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { register } from 'timeago.js';
import ru from 'timeago.js/esm/lang/ru';
import App from './App';
register('ru', ru);
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Для компонентов с частым рендером:
import { useMemo } from 'react';
import { format } from 'timeago.js';
function PostMeta({ createdAt }: { createdAt: string }) {
const label = useMemo(() => format(createdAt, 'ru'), [createdAt]);
return <time dateTime={createdAt}>{label}</time>;
}
Строка пересчитывается только при изменении createdAt.
Подходит для статичных списков.
import { format } from 'timeago.js';
interface Post {
id: number;
title: string;
createdAt: string;
}
function PostList({ posts }: { posts: Post[] }) {
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<time dateTime={post.createdAt}>
{format(post.createdAt, 'ru')}
</time>
</li>
))}
</ul>
);
}
import { useQuery } from '@tanstack/react-query';
import { format } from 'timeago.js';
function Feed() {
const { data } = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then(r => r.json()),
});
return (
<ul>
{data?.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<time dateTime={post.createdAt}>
{format(post.createdAt, 'ru')}
</time>
</li>
))}
</ul>
);
}