Создание React компонентов

Хорошо спроектированный React-компонент для относительного времени должен поддерживать автообновление, корректный cleanup, TypeScript и быть переиспользуемым.


Базовый компонент TimeAgo

import { format } from 'timeago.js';

interface TimeAgoProps {
  date: Date | string | number;
  locale?: string;
  className?: string;
}

function TimeAgo({ date, locale = 'ru', className }: TimeAgoProps) {
  return (
    <time
      dateTime={new Date(date).toISOString()}
      className={className}
    >
      {format(date, locale)}
    </time>
  );
}

export default TimeAgo;

Компонент с автообновлением

import { useState, useEffect } from 'react';
import { format } from 'timeago.js';

interface LiveTimeAgoProps {
  date: Date | string | number;
  locale?: string;
  updateInterval?: number;
}

function LiveTimeAgo({ date, locale = 'ru', updateInterval = 60000 }: LiveTimeAgoProps) {
  const [label, setLabel] = useState(() => format(date, locale));

  useEffect(() => {
    setLabel(format(date, locale));

    const id = setInterval(() => {
      setLabel(format(date, locale));
    }, updateInterval);

    return () => clearInterval(id);
  }, [date, locale, updateInterval]);

  return (
    <time dateTime={new Date(date).toISOString()}>
      {label}
    </time>
  );
}

Компонент с tooltip абсолютного времени

import { useMemo } from 'react';
import { format } from 'timeago.js';

interface TimeAgoWithTitleProps {
  date: string;
  locale?: string;
}

function TimeAgoWithTitle({ date, locale = 'ru' }: TimeAgoWithTitleProps) {
  const relative = useMemo(() => format(date, locale), [date, locale]);

  const absolute = useMemo(
    () => new Intl.DateTimeFormat('ru', {
      dateStyle: 'medium',
      timeStyle: 'short',
    }).format(new Date(date)),
    [date]
  );

  return (
    <time dateTime={date} title={absolute}>
      {relative}
    </time>
  );
}

Компонент с ограничением диапазона

import { format } from 'timeago.js';

interface SmartTimeProps {
  date: string;
  maxDays?: number;
  locale?: string;
}

function SmartTime({ date, maxDays = 30, locale = 'ru' }: SmartTimeProps) {
  const ageMs   = Date.now() - new Date(date).getTime();
  const ageDays = ageMs / 86400000;

  const label = ageDays > maxDays
    ? new Intl.DateTimeFormat('ru', { dateStyle: 'medium' }).format(new Date(date))
    : format(date, locale);

  return <time dateTime={date}>{label}</time>;
}

Компонент с семантикой svjezhe

import { format } from 'timeago.js';

type Freshness = 'hot' | 'fresh' | 'recent' | 'old';

function getFreshness(date: string): Freshness {
  const ms = Date.now() - new Date(date).getTime();
  if (ms < 300000)   return 'hot';    // < 5 мин
  if (ms < 3600000)  return 'fresh';  // < 1 ч
  if (ms < 86400000) return 'recent'; // < 1 д
  return 'old';
}

function TimeAgoTagged({ date }: { date: string }) {
  const freshness = getFreshness(date);

  return (
    <time
      dateTime={date}
      data-freshness={freshness}
      className={`time-ago time-ago--${freshness}`}
    >
      {format(date, 'ru')}
    </time>
  );
}

Компонент для комментариев

import { useState, useEffect } from 'react';
import { format } from 'timeago.js';

function CommentTime({ date }: { date: string }) {
  const [label, setLabel] = useState(() => format(date, 'ru'));

  useEffect(() => {
    const diff = Math.abs(Date.now() - new Date(date).getTime());

    // Обновлять только если дата свежая
    if (diff > 7 * 86400000) return;

    const interval = diff < 3600000 ? 60000 : 3600000;
    const id = setInterval(() => setLabel(format(date, 'ru')), interval);

    return () => clearInterval(id);
  }, [date]);

  return (
    <time dateTime={date} className="comment__date">
      {label}
    </time>
  );
}

Компонент с локалью из контекста

import { useContext } from 'react';
import { format } from 'timeago.js';
import { LocaleContext } from '../contexts/LocaleContext';

function ContextualTimeAgo({ date }: { date: string }) {
  const locale = useContext(LocaleContext);

  return (
    <time dateTime={date}>
      {format(date, locale)}
    </time>
  );
}

Компонент-скелетон с плейсхолдером

import { useState, useEffect } from 'react';
import { format } from 'timeago.js';

function TimeAgoWithSkeleton({ date }: { date: string | null }) {
  const [ready, setReady] = useState(false);
  const [label, setLabel] = useState('');

  useEffect(() => {
    if (!date) return;
    setLabel(format(date, 'ru'));
    setReady(true);
  }, [date]);

  if (!ready) {
    return <span className="skeleton skeleton-time" />;
  }

  return <time dateTime={date!}>{label}</time>;
}

Компонент memo для производительности

import { memo, useMemo } from 'react';
import { format } from 'timeago.js';

interface Props {
  date: string;
}

const TimeAgoMemo = memo(function TimeAgoMemo({ date }: Props) {
  const label = useMemo(() => format(date, 'ru'), [date]);
  return <time dateTime={date}>{label}</time>;
});

export default TimeAgoMemo;

memo предотвращает ненужные повторные рендеры при неизменной date.