Удаление обработчиков

Корректное удаление обработчиков и таймеров — важная часть работы с timeago.js. Без явной остановки таймеры render продолжают работать, что ведёт к утечкам памяти и ошибкам в приложении.


Что создаёт render

При вызове render(nodes, locale) для каждого элемента создаётся таймер через setTimeout. Этот таймер:

  • хранит ссылку на DOM-элемент;
  • вызывает себя рекурсивно через адаптивные интервалы;
  • не уничтожается автоматически при удалении элемента.

cancel — основной метод удаления

import { cancel } from 'timeago.js';

// Остановить все таймеры
cancel();

// Остановить для конкретного элемента
cancel(document.getElementById('time'));

// Остановить для коллекции
cancel(document.querySelectorAll('.time'));

Когда вызывать cancel

При размонтировании компонента:

// Жизненный цикл компонента
function onMount(el) {
  render(el, 'ru');
}

function onUnmount(el) {
  cancel(el); // обязательно
}

При навигации в SPA:

router.beforeEach((to, from, next) => {
  cancel(); // остановить все таймеры перед переходом
  next();
});

При скрытии контента:

function hideModal() {
  const times = modal.querySelectorAll('[datetime]');
  cancel(times);
  modal.style.display = 'none';
}

React: cleanup в useEffect

import { useEffect, useRef } from 'react';
import { render, cancel } from 'timeago.js';

function TimeAgo({ date }: { date: string }) {
  const ref = useRef<HTMLTimeElement>(null);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;

    render(el, 'ru');

    // Cleanup function — вызывается при размонтировании
    return () => {
      cancel(el);
    };
  }, [date]);

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

Функция, возвращаемая из useEffect, вызывается React автоматически при:

  • размонтировании компонента;
  • изменении зависимостей (здесь [date]).

Vue: onBeforeUnmount

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { render, cancel } from 'timeago.js';

const props = defineProps({ date: String });
const el = ref(null);

onMounted(() => render(el.value, 'ru'));
onBeforeUnmount(() => cancel(el.value));
</script>

Angular: ngOnDestroy

import { Component, OnInit, OnDestroy, ElementRef } from '@angular/core';
import { render, cancel } from 'timeago.js';

@Component({ template: '<time [attr.datetime]="date"></time>' })
export class TimeComponent implements OnInit, OnDestroy {
  date = '2025-05-26T10:00:00Z';

  constructor(private el: ElementRef) {}

  ngOnInit() {
    render(this.el.nativeElement.querySelector('time'), 'ru');
  }

  ngOnDestroy() {
    cancel(this.el.nativeElement.querySelector('time'));
  }
}

Svelte: onDestroy

<script>
  import { onMount, onDestroy } from 'svelte';
  import { render, cancel } from 'timeago.js';

  export let date;
  let el;

  onMount(() => render(el, 'ru'));
  onDestroy(() => cancel(el));
</script>

<time bind:this={el} datetime={date}></time>

Удаление при прокрутке (IntersectionObserver)

import { render, cancel } from 'timeago.js';

const observer = new IntersectionObserver((entries) => {
  entries.forEach(({ target, isIntersecting }) => {
    if (isIntersecting) {
      render(target, 'ru');
    } else {
      cancel(target); // убрать таймер для невидимого элемента
    }
  });
});

document.querySelectorAll('.time').forEach(el => observer.observe(el));

Удаление при удалении DOM-узла

import { cancel } from 'timeago.js';

const mo = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    mutation.removedNodes.forEach(node => {
      if (node.nodeType !== Node.ELEMENT_NODE) return;

      if (node.matches('[datetime]')) cancel(node);
      node.querySelectorAll?.('[datetime]').forEach(cancel);
    });
  });
});

mo.observe(document.body, { childList: true, subtree: true });

Глобальное уничтожение при unload

import { cancel } from 'timeago.js';

window.addEventListener('beforeunload', () => {
  cancel(); // освободить все таймеры
});

Проверка: что происходит без cancel

Если компонент с render размонтируется без cancel:

  1. Таймер продолжает существовать в памяти.
  2. При каждом срабатывании он пытается обновить удалённый элемент.
  3. Ссылка на DOM-элемент предотвращает его сборку GC.
  4. С каждым монтированием компонента количество таймеров растёт.

Это классическая утечка памяти в SPA-приложениях.