Vanilla Disintegratev1.0.0
RU
На этой странице

Реактивный фреймворк должен оставаться владельцем созданных им DOM-узлов. Передайте detach, чтобы Vanilla Disintegrate обновил состояние фреймворка вместо прямого вызова element.remove().

effects.remove(element, {
  effect: 'dust',
  detach: () => removeFromFrameworkState(),
});

Хук вызывается после измерения и создания снимка — ровно в момент фиксации удаления. Без detach библиотека, как обычно, вызывает element.remove() сама.

Для восстановления сначала отрендерите элемент, дождитесь его добавления в DOM и затем передайте новую ссылку в restore(). В состоянии приложения следует хранить данные, а не отсоединённый DOM-узел, поэтому в реактивных интеграциях retain обычно остаётся равным false.

Это новый DOM-узел, поэтому явно передайте тот же effect в restore(), если при удалении использовался не эффект экземпляра по умолчанию. Запомненный эффект может перейти автоматически только вместе с тем же сохранённым DOM-узлом.

React

React должен синхронно удалить управляемый им узел. flushSync() явно задаёт эту границу, а useLayoutEffect() запускает восстановление после вставки нового измеримого узла.

'use client';

import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { flushSync } from 'react-dom';
import Disintegrator from 'vanilla-disintegrate/snapdom';

export function DisintegratingCard() {
  const [visible, setVisible] = useState(true);
  const cardRef = useRef<HTMLElement | null>(null);
  const effectsRef = useRef<Disintegrator | null>(null);
  const pendingRestore = useRef(false);

  useEffect(() => {
    const effects = new Disintegrator();
    effectsRef.current = effects;

    return () => {
      effects.destroy();
      effectsRef.current = null;
    };
  }, []);

  useLayoutEffect(() => {
    const effects = effectsRef.current;
    const element = cardRef.current;

    if (!visible || !pendingRestore.current || !effects || !element) return;
    pendingRestore.current = false;
    effects.restore(element, { effect: 'dust' });
  }, [visible]);

  function removeCard() {
    const effects = effectsRef.current;
    const element = cardRef.current;
    if (!effects || !element) return;

    effects.remove(element, {
      effect: 'dust',
      detach: () => flushSync(() => setVisible(false)),
    });
  }

  function restoreCard() {
    pendingRestore.current = true;
    setVisible(true);
  }

  return visible ? (
    <article ref={cardRef}>
      <h2>Активность</h2>
      <button onClick={removeCard}>Удалить</button>
    </article>
  ) : (
    <button onClick={restoreCard}>Вернуть</button>
  );
}

Создавайте экземпляр в effect и вызывайте destroy() в cleanup. Такая схема корректно переживает React Strict Mode. В Next.js и других React SSR-фреймворках компонент должен быть клиентским.

Vue

Vue применяет изменение состояния до следующей отрисовки браузера. После nextTick() функция restore() получает уже отрендеренный элемент с итоговой геометрией.

<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, ref } from 'vue';
import Disintegrator from 'vanilla-disintegrate/snapdom';

const visible = ref(true);
const card = ref<HTMLElement | null>(null);
let effects: Disintegrator | null = null;

onMounted(() => {
  effects = new Disintegrator();
});

onUnmounted(() => {
  effects?.destroy();
});

function removeCard() {
  if (!effects || !card.value) return;

  effects.remove(card.value, {
    effect: 'dust',
    detach: () => {
      visible.value = false;
    },
  });
}

async function restoreCard() {
  visible.value = true;
  await nextTick();

  if (effects && card.value) {
    effects.restore(card.value, { effect: 'dust' });
  }
}
</script>

<template>
  <article v-if="visible" ref="card">
    <h2>Активность</h2>
    <button @click="removeCard">Удалить</button>
  </article>
  <button v-else @click="restoreCard">Вернуть</button>
</template>

Компонент подходит и для Nuxt: browser-only инициализация находится внутри onMounted().

Svelte

Получите живой узел через bind:this, верните destroy() из onMount() и дождитесь tick() перед восстановлением.

<script lang="ts">
  import { onMount, tick } from 'svelte';
  import Disintegrator from 'vanilla-disintegrate/snapdom';

  let visible = true;
  let card: HTMLElement;
  let effects: Disintegrator | null = null;

  onMount(() => {
    effects = new Disintegrator();
    return () => effects?.destroy();
  });

  function removeCard() {
    if (!effects || !card) return;

    effects.remove(card, {
      effect: 'dust',
      detach: () => {
        visible = false;
      },
    });
  }

  async function restoreCard() {
    visible = true;
    await tick();

    if (effects && card) {
      effects.restore(card, { effect: 'dust' });
    }
  }
</script>

{#if visible}
  <article bind:this={card}>
    <h2>Активность</h2>
    <button onclick={removeCard}>Удалить</button>
  </article>
{:else}
  <button onclick={restoreCard}>Вернуть</button>
{/if}

В SvelteKit эта схема также безопасна для SSR: onMount() выполняется только в браузере.

Solid

Solid синхронно обновляет ветку, управляемую сигналом. Микрозадача после setVisible(true) гарантирует, что восстановление увидит вставленный узел.

import { createSignal, onCleanup, onMount, Show } from 'solid-js';
import Disintegrator from 'vanilla-disintegrate/snapdom';

export function DisintegratingCard() {
  const [visible, setVisible] = createSignal(true);
  let card!: HTMLElement;
  let effects: Disintegrator | null = null;

  onMount(() => {
    effects = new Disintegrator();
  });

  onCleanup(() => effects?.destroy());

  function removeCard() {
    if (!effects || !card) return;

    effects.remove(card, {
      effect: 'dust',
      detach: () => setVisible(false),
    });
  }

  function restoreCard() {
    setVisible(true);
    queueMicrotask(() => effects?.restore(card, { effect: 'dust' }));
  }

  return (
    <Show when={visible()} fallback={<button onClick={restoreCard}>Вернуть</button>}>
      <article ref={card}>
        <h2>Активность</h2>
        <button onClick={removeCard}>Удалить</button>
      </article>
    </Show>
  );
}

В SolidStart используйте тот же клиентский lifecycle.

Angular

Angular Signals позволяют зафиксировать условное представление внутри detach. afterNextRender() исключает создание экземпляра во время SSR, а DestroyRef управляет очисткой.

import {
  afterNextRender,
  ChangeDetectorRef,
  Component,
  DestroyRef,
  ElementRef,
  inject,
  signal,
  viewChild,
} from '@angular/core';
import Disintegrator from 'vanilla-disintegrate/snapdom';

@Component({
  selector: 'app-disintegrating-card',
  standalone: true,
  template: `
    @if (visible()) {
      <article #card>
        <h2>Активность</h2>
        <button (click)="removeCard()">Удалить</button>
      </article>
    } @else {
      <button (click)="restoreCard()">Вернуть</button>
    }
  `,
})
export class DisintegratingCard {
  readonly visible = signal(true);
  readonly card = viewChild<ElementRef<HTMLElement>>('card');

  private readonly changeDetector = inject(ChangeDetectorRef);
  private readonly destroyRef = inject(DestroyRef);
  private effects: Disintegrator | null = null;

  constructor() {
    afterNextRender(() => {
      this.effects = new Disintegrator();
    });
    this.destroyRef.onDestroy(() => this.effects?.destroy());
  }

  removeCard() {
    const element = this.card()?.nativeElement;
    if (!this.effects || !element) return;

    this.effects.remove(element, {
      effect: 'dust',
      detach: () => {
        this.visible.set(false);
        this.changeDetector.detectChanges();
      },
    });
  }

  restoreCard() {
    this.visible.set(true);
    this.changeDetector.detectChanges();

    const element = this.card()?.nativeElement;
    if (this.effects && element) {
      this.effects.restore(element, { effect: 'dust' });
    }
  }
}

В любом интерфейсе с DOM

Vanilla Disintegrate работает в любом браузерном интерфейсе, где есть измеримые DOM-элементы. Ей не нужен фреймворк, адаптер или отдельный пакет интеграции.

React, Vue, Svelte, Solid и Angular выше — не перечень поддерживаемых платформ, а примеры их типичных lifecycle-сценариев. Тот же подход подходит для любого другого фреймворка, библиотеки или приложения на чистом JavaScript.

Примеры используют официальные DOM и lifecycle API: React refs и effects, template refs и lifecycle Vue, lifecycle Svelte, refs и cleanup Solid, lifecycle Angular.

Редактировать страницу на GitHub