Hydration
Hydration é o processo de “reanimar” o HTML estático renderizado no servidor, reconectando os estados reativos e event handlers no navegador.
O que é Hydration?
Section titled “O que é Hydration?”Quando você usa SSR, o servidor envia HTML completo para o navegador. Mas esse HTML é “estático” - não tem reatividade nem event handlers funcionando. A hidratação resolve isso:
- O navegador recebe HTML do servidor
- O HTML é exibido imediatamente (rápido!)
- JavaScript carrega e “hidrata” o DOM existente
- Estados reativos são reconectados
- Event handlers começam a funcionar
- A aplicação fica totalmente interativa
Benefícios da Hydration
Section titled “Benefícios da Hydration”- ✅ Performance: Conteúdo visível antes do JS carregar
- ✅ SEO: Crawlers veem HTML completo
- ✅ UX Progressiva: Funciona mesmo com JS desabilitado (conteúdo básico)
- ✅ Eficiência: Reutiliza DOM existente (não recria tudo)
Como Funciona no Slash
Section titled “Como Funciona no Slash”O Slash usa marcadores especiais no HTML para identificar partes reativas:
1. Marcadores de Texto Reativo
Section titled “1. Marcadores de Texto Reativo”<!-- HTML renderizado no servidor --><div> Contador: <!--reactive-start:s0-->42<!--reactive-end:s0--></div>Os comentários <!--reactive-start:s0--> e <!--reactive-end:s0--> marcam onde o estado reativo s0 está sendo usado.
2. Marcadores de Atributos Reativos
Section titled “2. Marcadores de Atributos Reativos”<!-- HTML renderizado no servidor --><button class="active" data-reactive-class="s0">Click</button><input value="John" data-reactive-value="s1" /><input type="checkbox" checked data-reactive-checked="s2" />Os atributos data-reactive-* indicam que esses atributos devem ser reconectados aos estados reativos.
3. Estado Serializado
Section titled “3. Estado Serializado”O servidor injeta o estado inicial no HTML:
<script id="__SLASH_STATE__" type="application/json">{"s0":42,"s1":"John","s2":true}</script>Funções de Hydration
Section titled “Funções de Hydration”hydrateReactiveAttributes()
Section titled “hydrateReactiveAttributes()”Hidrata atributos reativos de um elemento específico.
import { hydrateReactiveAttributes, createState } from '@ezbug/slash'
// Recuperar estados serializadosconst stateData = JSON.parse( document.getElementById('__SLASH_STATE__')?.textContent || '{}')
// Recriar states a partir dos dadosconst reactives = new Map()reactives.set('s0', createState({ value: stateData.s0 }))
// Hidratar elementoconst button = document.querySelector('button')hydrateReactiveAttributes(button, reactives)hydrateReactiveNodes()
Section titled “hydrateReactiveNodes()”Hidrata nós de texto reativos (marcados com comentários):
import { hydrateReactiveNodes, createState } from '@ezbug/slash'
// Recuperar estadosconst stateData = JSON.parse( document.getElementById('__SLASH_STATE__')?.textContent || '{}')
const reactives = new Map()reactives.set('s0', createState({ value: stateData.s0 }))
// Hidratar todos os nós de texto reativos no containerconst container = document.getElementById('app')hydrateReactiveNodes(container, reactives)walkAndHydrateReactiveAttributes()
Section titled “walkAndHydrateReactiveAttributes()”Percorre recursivamente a árvore DOM e hidrata todos os atributos reativos:
import { walkAndHydrateReactiveAttributes, createState } from '@ezbug/slash'
const stateData = JSON.parse( document.getElementById('__SLASH_STATE__')?.textContent || '{}')
const reactives = new Map()reactives.set('s0', createState({ value: stateData.s0 }))
const root = document.getElementById('app')walkAndHydrateReactiveAttributes(root, reactives)Exemplo Completo de Hydration
Section titled “Exemplo Completo de Hydration”Servidor (SSR)
Section titled “Servidor (SSR)”import { renderToString, htmlString, createState } from '@ezbug/slash'
const Counter = () => { const count = createState({ value: 0 }) const { value } = count.get()
return htmlString` <div id="app"> <h1>Contador: ${value}</h1> <button class="increment">+1</button> </div> `}
const { html, state } = renderToString(Counter)
const fullHtml = `<!DOCTYPE html><html> <head> <title>SSR + Hydration</title> </head> <body> ${html} <script id="__SLASH_STATE__" type="application/json"> ${JSON.stringify(state)} </script> <script src="/client.js" type="module"></script> </body></html>`
// Enviar fullHtml para o navegadorCliente (Hydration)
Section titled “Cliente (Hydration)”import { createState, hydrateReactiveNodes, walkAndHydrateReactiveAttributes} from '@ezbug/slash'
// 1. Recuperar estado serializadoconst stateScript = document.getElementById('__SLASH_STATE__')const stateData = JSON.parse(stateScript?.textContent || '{}')
// 2. Recriar estados reativosconst count = createState({ value: stateData.s0 || 0 })
// 3. Mapear IDs para statesconst reactives = new Map()reactives.set('s0', count)
// 4. Hidratar nós de texto reativosconst app = document.getElementById('app')if (app) { hydrateReactiveNodes(app, reactives) walkAndHydrateReactiveAttributes(app, reactives)}
// 5. Adicionar event handlers (não são serializados no SSR)const button = document.querySelector('.increment')button?.addEventListener('click', () => { count.set({ value: count.get().value + 1 })})Context de Hydration
Section titled “Context de Hydration”Para casos mais avançados, você pode usar o contexto de hidratação:
setHydrateContext()
Section titled “setHydrateContext()”Define o contexto de hidratação para reutilizar DOM existente:
import { setHydrateContext, render, html } from '@ezbug/slash'
// Definir contexto apontando para o DOM existenteconst root = document.getElementById('app')setHydrateContext({ cursor: root?.firstChild || null})
// Renderizar (vai reutilizar DOM existente)const App = () => html` <div> <h1>Hidratado!</h1> </div>`
render(App, root)
// Limpar contextosetHydrateContext(null)getHydrateContext()
Section titled “getHydrateContext()”Obtém o contexto de hidratação atual:
import { getHydrateContext } from '@ezbug/slash'
const context = getHydrateContext()
if (context) { console.log('Estamos em modo de hidratação') console.log('Cursor atual:', context.cursor)}Hydration com hHydrate()
Section titled “Hydration com hHydrate()”A função hHydrate() é uma versão especial do h() que reutiliza DOM existente:
import { hHydrate, setHydrateContext } from '@ezbug/slash'
// Configurar contextoconst root = document.getElementById('app')setHydrateContext({ cursor: root?.firstChild || null })
// Hidratar elementosconst element = hHydrate('div', { class: 'container' }, hHydrate('h1', null, 'Título'), hHydrate('p', null, 'Parágrafo'))
// Limpar contextosetHydrateContext(null)Hydration Automática com render()
Section titled “Hydration Automática com render()”Você pode simplificar o processo usando render() com hydration automática:
Servidor
Section titled “Servidor”import { renderToString, htmlString } from '@ezbug/slash'
const App = () => htmlString` <div id="app"> <h1>My App</h1> </div>`
const { html, state } = renderToString(App)
// Enviar html + state para o navegadorCliente
Section titled “Cliente”import { render, html, createState, hydrateReactiveNodes } from '@ezbug/slash'
// Recuperar estadoconst stateData = JSON.parse( document.getElementById('__SLASH_STATE__')?.textContent || '{}')
// Criar map de estadosconst reactives = new Map()Object.entries(stateData).forEach(([id, value]) => { reactives.set(id, createState({ value }))})
// Hidratarconst root = document.getElementById('app')if (root) { hydrateReactiveNodes(root, reactives)}
// Renderizar normalmente (reutiliza DOM)const App = () => html`<div id="app"><h1>My App</h1></div>`render(App, root)Exemplo Avançado: Lista Reativa
Section titled “Exemplo Avançado: Lista Reativa”Servidor
Section titled “Servidor”import { renderToString, htmlString, createState } from '@ezbug/slash'
type Todo = { id: number; text: string; done: boolean }
const todos = createState<Todo[]>({ value: [ { id: 1, text: 'Learn SSR', done: true }, { id: 2, text: 'Learn Hydration', done: false } ]})
const TodoApp = () => { const { value: items } = todos.get()
return htmlString` <div id="app"> <h1>Todo List</h1> <ul> ${items.map(todo => htmlString` <li class=${todo.done ? 'done' : ''}> <input type="checkbox" checked=${todo.done} data-id="${todo.id}" /> <span>${todo.text}</span> </li> `)} </ul> <button class="add">Add Todo</button> </div> `}
const { html, state } = renderToString(TodoApp)Cliente
Section titled “Cliente”import { createState, hydrateReactiveNodes, walkAndHydrateReactiveAttributes} from '@ezbug/slash'
type Todo = { id: number; text: string; done: boolean }
// Recuperar estadoconst stateData = JSON.parse( document.getElementById('__SLASH_STATE__')?.textContent || '{}')
// Recriar stateconst todos = createState<Todo[]>({ value: stateData.s0 || [] })
// Map de reactivesconst reactives = new Map()reactives.set('s0', todos)
// Hidratarconst app = document.getElementById('app')if (app) { hydrateReactiveNodes(app, reactives) walkAndHydrateReactiveAttributes(app, reactives)}
// Event handlersapp?.addEventListener('change', (e) => { const target = e.target as HTMLInputElement if (target.type === 'checkbox') { const id = Number(target.dataset.id) const current = todos.get().value todos.set({ value: current.map(t => t.id === id ? { ...t, done: target.checked } : t ) }) }})
const addBtn = app?.querySelector('.add')addBtn?.addEventListener('click', () => { const current = todos.get().value const newTodo: Todo = { id: Date.now(), text: `Todo ${current.length + 1}`, done: false } todos.set({ value: [...current, newTodo] })})Utilitários de Hydration
Section titled “Utilitários de Hydration”skipReactiveMarkers()
Section titled “skipReactiveMarkers()”Pula marcadores reativos durante o percorrimento do DOM:
import { skipReactiveMarkers, getHydrateContext } from '@ezbug/slash'
// Durante a hidratação, pula comentários reactive-start/endconst context = getHydrateContext()if (context?.cursor?.nodeType === Node.COMMENT_NODE) { skipReactiveMarkers()}hydrateChild()
Section titled “hydrateChild()”Hidrata um child individual durante o processo:
import { hydrateChild } from '@ezbug/slash'
// Hidratar um child específicohydrateChild(someChildNode)Fluxo Completo SSR + Hydration
Section titled “Fluxo Completo SSR + Hydration”1. Servidor Renderiza
Section titled “1. Servidor Renderiza”const { html, state } = renderToString(App)// html: HTML estático com marcadores// state: { s0: valor0, s1: valor1, ... }2. HTML é Enviado ao Navegador
Section titled “2. HTML é Enviado ao Navegador”<div id="app"> Contador: <!--reactive-start:s0-->42<!--reactive-end:s0--> <button class="btn" data-reactive-class="s1">Click</button></div><script id="__SLASH_STATE__" type="application/json">{"s0":42,"s1":"active"}</script>3. Navegador Exibe HTML Imediatamente
Section titled “3. Navegador Exibe HTML Imediatamente”O usuário vê o conteúdo antes do JavaScript carregar.
4. JavaScript Carrega e Hidrata
Section titled “4. JavaScript Carrega e Hidrata”// Recuperar estadoconst stateData = JSON.parse( document.getElementById('__SLASH_STATE__')?.textContent || '{}')
// Recriar statesconst count = createState({ value: stateData.s0 })const btnClass = createState({ value: stateData.s1 })
const reactives = new Map([ ['s0', count], ['s1', btnClass]])
// Hidratarconst app = document.getElementById('app')hydrateReactiveNodes(app, reactives)walkAndHydrateReactiveAttributes(app, reactives)
// Reconectar event handlersconst btn = app?.querySelector('.btn')btn?.addEventListener('click', () => { count.set({ value: count.get().value + 1 })})5. Aplicação Totalmente Interativa
Section titled “5. Aplicação Totalmente Interativa”Agora os estados reativos estão funcionando e a aplicação responde a eventos.
Boas Práticas
Section titled “Boas Práticas”✅ Serializar Estado Corretamente
Section titled “✅ Serializar Estado Corretamente”// Servidor: incluir estado no HTMLconst { html, state } = renderToString(App)
const fullHtml = ` ${html} <script id="__SLASH_STATE__" type="application/json"> ${JSON.stringify(state)} </script>`✅ Manter Estrutura DOM Idêntica
Section titled “✅ Manter Estrutura DOM Idêntica”O HTML renderizado no cliente deve ter a mesma estrutura do servidor:
// ❌ ERRADO: Estruturas diferentes// Servidor: htmlString`<div><h1>Title</h1></div>`// Cliente: html`<div><p>Title</p></div>`
// ✅ CORRETO: Mesma estrutura// Servidor: htmlString`<div><h1>Title</h1></div>`// Cliente: html`<div><h1>Title</h1></div>`✅ Hidratar Antes de Adicionar Event Handlers
Section titled “✅ Hidratar Antes de Adicionar Event Handlers”// 1. Hidratar primeirohydrateReactiveNodes(app, reactives)walkAndHydrateReactiveAttributes(app, reactives)
// 2. Depois adicionar event handlersbutton.addEventListener('click', handleClick)✅ Limpar Script de Estado
Section titled “✅ Limpar Script de Estado”// Remover script após hidratar (opcional, para limpeza)const stateScript = document.getElementById('__SLASH_STATE__')stateScript?.remove()❌ Evitar Renderizar do Zero
Section titled “❌ Evitar Renderizar do Zero”// ❌ ERRADO: Recria todo o DOMconst root = document.getElementById('app')root.innerHTML = '' // Joga fora o DOM do servidor!render(App, root)
// ✅ CORRETO: Reutiliza DOM existentehydrateReactiveNodes(root, reactives)Debugging Hydration
Section titled “Debugging Hydration”Ver Marcadores Reativos
Section titled “Ver Marcadores Reativos”Abra DevTools e inspecione o HTML:
<div> Count: <!--reactive-start:s0-->42<!--reactive-end:s0--></div>Verificar Estado Serializado
Section titled “Verificar Estado Serializado”const stateData = JSON.parse( document.getElementById('__SLASH_STATE__')?.textContent || '{}')console.log('Estado do servidor:', stateData)Verificar Atributos Reativos
Section titled “Verificar Atributos Reativos”const elements = document.querySelectorAll('[data-reactive-class]')console.log('Elementos com classes reativas:', elements)Limitações
Section titled “Limitações”- Event Handlers não são serializados: Você precisa reconectá-los manualmente no cliente
- Estrutura DOM deve ser idêntica: Diferenças entre servidor/cliente causam problemas
- Funções não podem ser serializadas: Apenas dados primitivos e objetos simples
Próximos Passos
Section titled “Próximos Passos”- Veja Universal Data Loading para data fetching isomórfico
- Explore SSR para entender a renderização no servidor
- Confira exemplos práticos no template slash-ssr