Паттерн реального времени — архитектурный подход, при котором временные метки автоматически обновляются по мере течения времени без участия пользователя.
Пользователь видит “2 минуты назад” сразу после загрузки. Через минуту без перезагрузки страницы надпись меняется на “3 минуты назад”. Через 57 минут — “час назад”.
timeago.js реализует это через функцию render, которая
создаёт внутренние таймеры с адаптивным интервалом:
<time datetime="2025-06-01T11:55:00Z">только что</time>
import { render, cancel } from 'timeago.js';
import ru from 'timeago.js/esm/lang/ru';
import { register } from 'timeago.js';
register('ru', ru);
const el = document.querySelector('time[datetime]');
render(el, 'ru');
// При уходе со страницы
window.addEventListener('unload', () => cancel(el));
import { useEffect, useRef } from 'react';
import { render, cancel } from 'timeago.js';
interface LiveTimeProps {
date: Date | string;
locale?: string;
}
export function LiveTime({ date, locale = 'ru' }: LiveTimeProps) {
const ref = useRef<HTMLTimeElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
render(el, locale);
return () => cancel(el);
}, [locale]);
const isoDate = new Date(date as any).toISOString();
return (
<time
ref={ref}
dateTime={isoDate}
/>
);
}
<template>
<time :datetime="isoDate" ref="timeEl" />
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { render, cancel } from 'timeago.js';
const props = defineProps<{ date: string | Date; locale?: string }>();
const timeEl = ref<HTMLTimeElement | null>(null);
const isoDate = computed(() =>
new Date(props.date as any).toISOString()
);
onMounted(() => {
if (timeEl.value) render(timeEl.value, props.locale ?? 'ru');
});
onUnmounted(() => {
if (timeEl.value) cancel(timeEl.value);
});
</script>
// React
export function PostFeed({ posts }: { posts: Post[] }) {
const containerRef = useRef<HTMLUListElement>(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const times = container.querySelectorAll('time[datetime]');
render(Array.from(times), 'ru');
return () => cancel(Array.from(times));
}, [posts]);
return (
<ul ref={containerRef}>
{posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<time dateTime={new Date(post.createdAt).toISOString()} />
</li>
))}
</ul>
);
}
import { render, cancel } from 'timeago.js';
const elements = document.querySelectorAll('[datetime]');
function startUpdates() { render(elements, 'ru'); }
function pauseUpdates() { cancel(elements); }
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
startUpdates();
} else {
pauseUpdates();
}
});
startUpdates(); // При загрузке
При получении новых данных через WebSocket перезапустить render для новых элементов:
const ws = new WebSocket('wss://api.example.com/feed');
ws.onmess age = (event) => {
const post = JSON.parse(event.data);
const newEl = createPostElement(post);
document.getElementById('feed').prepend(newEl);
// Запустить timeago для нового элемента
const timeEl = newEl.querySelector('time');
if (timeEl) render(timeEl, 'ru');
};
import { render, cancel } from 'timeago.js';
const io = new IntersectionObserver((entries) => {
entries.forEach(({ target, isIntersecting }) => {
if (isIntersecting) {
render(target, 'ru');
} else {
cancel(target);
}
});
}, { threshold: 0.1 });
document.querySelectorAll('[datetime]').forEach(el => io.observe(el));
function HybridTimeAgo({ date, locale = 'ru' }: { date: string; locale?: string }) {
const [mounted, setMounted] = useState(false);
const ref = useRef<HTMLTimeElement>(null);
useEffect(() => {
setMounted(true);
if (ref.current) {
render(ref.current, locale);
return () => { if (ref.current) cancel(ref.current); };
}
}, [locale]);
return (
<time
ref={ref}
dateTime={date}
// Fallback до монтирования (SSR)
>
{!mounted && new Date(date).toLocaleDateString('ru-RU')}
</time>
);
}