Skip to content

Lifecycle e Cleanup

O Slash não possui lifecycle hooks como useEffect, onMounted ou onCleanup. O ciclo de vida de um componente é simples:

  1. Montagem: ao ser montado como <${Counter} />, o componente executa e retorna um nó DOM. Cada state.get() chamado nessa execução registra o state como dependência do componente.
  2. Re-render: quando um desses states muda, o componente executa de novo, os nós anteriores são destruídos e os novos os substituem.
  3. Remoção: quando os nós saem da tela por um re-render ou por render() sobre o mesmo container, o Slash destrói os nós antigos (veja abaixo).
import { html, createState, render } from '@_bashell/slash'
// O state fica fora do componente: se fosse criado dentro, seria recriado a cada render
const count = createState(0)
function Counter() {
// Ler count.get() durante a execução inscreve o componente no state
return html`
<div>
<p>Contador: ${count.get()}</p>
<button onClick=${() => count.set(count.get() + 1)}>+1</button>
</div>
`
}
// Monte como <${Counter} />. Chamar Counter() diretamente não é reativo
render(html`<${Counter} />`, '#app')

O rastreamento é refeito a cada execução

Section titled “O rastreamento é refeito a cada execução”

A cada execução o componente refaz a lista de states que lê: assina os recém-lidos e deixa de observar os que não leu mais. Um state lido só depois de um retorno antecipado passa a ser observado assim que esse ramo executa:

const Profile = () => {
if (loading.get()) return html`<p>Carregando...</p>` // 1ª execução: observa só `loading`
return html`<p>${data.get()}</p>` // depois de loading=false: também observa `data`
}

A exceção é um componente cuja primeira execução não leu nenhum state: ele é estático (devolvido como nó simples) e nunca re-executa, mesmo que depois fosse ler states.

Internamente o Slash guarda funções de limpeza por nó DOM (num WeakMap) e as executa quando o nó é destruído. Isso cobre o que o próprio Slash cria:

  • listeners de eventos registrados por props como onClick;
  • a inscrição de um componente nos states que ele leu;
  • inscrições de filhos e props reativos (Router, controles de formulário).

Os nós são destruídos quando:

  1. um componente re-renderiza e substitui seus nós antigos;
  2. render(view, container) é chamado sobre um container que já tem conteúdo;
  3. o Router troca a rota exibida.

destroyNode(node) é a única peça desse mecanismo que é pública (exportada por @_bashell/slash e @_bashell/slash/core). Ela executa as limpezas registradas pelo Slash no nó e em todos os descendentes. Não remove o nó do DOM.

import { html, render, destroyNode } from '@_bashell/slash'
const node = html`<button onClick=${() => console.log('oi')}>Oi</button>` as HTMLElement
document.body.appendChild(node)
// Remover manualmente: destrua primeiro (remove os listeners), depois tire do DOM
destroyNode(node)
node.remove()

Use destroyNode quando você remover à mão, com removeChild()/remove(), nós que o Slash criou.

Na prática isso significa que recursos externos (timers, WebSocket, EventSource, listeners em window) ficam sob sua responsabilidade.

Mantenha o recurso fora do componente e controle início e fim explicitamente.

watch() devolve a função de cancelamento

Section titled “watch() devolve a função de cancelamento”
const count = createState(0)
const unwatch = count.watch((value) => {
console.log('count mudou para', value)
})
// quando não precisar mais
unwatch()

Crie o watch() uma vez (no módulo ou em um handler), nunca no corpo de um componente, que executa várias vezes e acumularia watchers.

import { html, createState, render } from '@_bashell/slash'
const seconds = createState(0)
let timer: ReturnType<typeof setInterval> | null = null
const start = () => {
if (timer) return
timer = setInterval(() => seconds.set(seconds.get() + 1), 1000)
}
const stop = () => {
if (timer) clearInterval(timer)
timer = null
}
const Clock = () => html`
<div>
<p>${seconds.get()}s</p>
<button onClick=${start}>Iniciar</button>
<button onClick=${stop}>Parar</button>
</div>
`
render(html`<${Clock} />`, '#app')

O mesmo vale para fetch com AbortController, WebSocket e listeners em window: crie e encerre em funções que você controla (handlers, ou o código que chama render()), não dentro do corpo do componente.

Para executar um efeito quando um state muda, use watch() fora do componente:

import { createState } from '@_bashell/slash'
type User = { id: number; name: string }
const userId = createState<number | null>(null)
const userData = createState<User | null>(null)
userId.watch(async (id) => {
if (id === null) return
const user = await fetch(`/api/users/${id}`).then((r) => r.json())
userData.set(user)
})
FrameworkHooks de lifecycleCleanup
SlashNenhumAutomático para o que o Slash cria; destroyNode() para remoção manual; recursos externos por sua conta
ReactuseEffectFunção retornada
VueonMounted, onUnmountedonUnmounted()
SolidonMount, onCleanuponCleanup()