Хуки для React

Пользовательские хуки позволяют инкапсулировать логику обновления относительного времени и переиспользовать её в компонентах.


Базовый хук useTimeAgo

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;
}

Хук с paused состоянием

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) };
}

Хук с синхронизацией locale из контекста

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-безопасностью

При 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;
}

Пустая строка на сервере предотвращает гидрационный конфликт.


Хук useRelativeDateList с группировкой

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]);
}