Реактивный фреймворк должен оставаться владельцем созданных им 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.