Batch Updates
Função batch() para Agrupar Atualizações
Section titled “Função batch() para Agrupar Atualizações”A função batch() agrupa múltiplas atualizações de estado, notificando watchers apenas uma vez ao final do batch em vez de notificar após cada set().
Assinatura
Section titled “Assinatura”function batch(fn: () => void): voidParâmetros:
fn: Função que contém as atualizações de estado a serem agrupadas
Retorno: void
Uso Básico
Section titled “Uso Básico”import { createState, batch } from '@_bashell/slash'
const count = createState(0)const name = createState('Alice')
count.watch(() => console.log('Count changed'))name.watch(() => console.log('Name changed'))
// Sem batch: 2 notificaçõescount.set(5) // Log: "Count changed"name.set('Bob') // Log: "Name changed"
// Com batch: 2 notificações agrupadasbatch(() => { count.set(10) // Não notifica ainda name.set('Charlie') // Não notifica ainda}) // Notifica ambos aqui// Log: "Count changed"// Log: "Name changed"Implementação: src/batch.ts
Como Funciona e Quando Usar
Section titled “Como Funciona e Quando Usar”Como Funciona
Section titled “Como Funciona”- Início do Batch:
batch()marca o contexto global como “batching” - Acúmulo: Cada
state.set()registra um update mas não notifica watchers - Fim do Batch: Ao finalizar a função, todos os watchers são notificados uma vez
const state = createState({ count: 0, name: 'John' })
state.watch((value) => { console.log('State changed:', value)})
batch(() => { state.set({ count: 1, name: 'John' }) // Registrado state.set({ count: 2, name: 'John' }) // Registrado state.set({ count: 3, name: 'Jane' }) // Registrado})// Log: "State changed: { count: 3, name: 'Jane' }" (apenas uma vez)Semântica
Section titled “Semântica”- Só são notificados os states que mudaram de valor dentro do batch. States não alterados não notificam, e um batch em que nenhum
setmudou valor não notifica ninguém. - Cada state alterado notifica uma única vez, com o valor final, na ordem em que foi alterado pela primeira vez. Qualquer
setque muda o valor marca o state como pendente, mesmo que ele volte ao valor inicial. - Batches aninhados são suportados: o fim do batch interno não encerra o externo, e as notificações só ocorrem no fim do batch mais externo.
- Se a função lançar um erro, as notificações ainda acontecem e o erro é propagado.
- Um
setfeito por um watcher durante as notificações (já fora do batch) notifica normalmente. - Esse
setsegue a regra “o último valor vence”: nenhum watcher recebe um valor velho depois do novo. Veja Estado. - Erros em watchers são isolados, dentro e fora de batch: todos os watchers rodam e o primeiro erro é relançado no final. Se
fne um watcher lançarem, o erro defné o propagado e o do watcher é registrado comconsole.error("[slash] erro em watcher durante o flush do batch", err). - Se um watcher altera um state cuja notificação ainda estava pendente no mesmo flush, esse state notifica uma única vez com o valor mais recente (sem duplicar).
Estado Interno
Section titled “Estado Interno”Batch mantém um contador de profundidade e uma fila de notificadores pendentes:
function enterBatch(depth: number): numberfunction exitBatch(depth: number): { depth: number; flush: boolean }Fluxo:
batch()called → profundidade++state.set()que muda o valor → enfileira o notificador do state (deduplicado)- Batch termina → profundidade—; se voltou a 0, cada notificador pendente roda uma vez
Implementação Core: src/batch-core.ts
Quando Usar
Section titled “Quando Usar”Use batch() quando você precisa atualizar múltiplos states ou o mesmo state várias vezes:
1. Atualizar Múltiplos States
Section titled “1. Atualizar Múltiplos States”const user = createState({ name: '', age: 0 })const isLoading = createState(false)const error = createState<string | null>(null)
// ❌ Sem batch: 3 notificações separadasconst loadUser = async (id: number) => { isLoading.set(true) error.set(null)
try { const data = await fetchUser(id) user.set(data) } catch (err) { error.set(err.message) } finally { isLoading.set(false) }}
// ✅ Com batch: as atualizações de cada etapa são notificadas juntasconst loadUser = async (id: number) => { batch(() => { isLoading.set(true) error.set(null) })
try { const data = await fetchUser(id) batch(() => { user.set(data) isLoading.set(false) }) } catch (err) { batch(() => { error.set(err.message) isLoading.set(false) }) }}2. Loops com Múltiplas Atualizações
Section titled “2. Loops com Múltiplas Atualizações”const items = createState<number[]>([])
// ❌ Sem batch: 1000 notificaçõesfor (let i = 0; i < 1000; i++) { items.set([...items.get(), i])}
// ✅ Com batch: 1 notificaçãobatch(() => { for (let i = 0; i < 1000; i++) { items.set([...items.get(), i]) }})
// ✅ Ainda melhor: Acumular em variável localconst newItems = []for (let i = 0; i < 1000; i++) { newItems.push(i)}items.set(newItems) // 1 notificação, sem batch necessário3. Inicialização de Múltiplos States
Section titled “3. Inicialização de Múltiplos States”const formState = createState({ email: '', password: '' })const validationErrors = createState({})const isSubmitting = createState(false)
const initializeForm = () => { batch(() => { formState.set({ email: '', password: '' }) validationErrors.set({}) isSubmitting.set(false) })}4. Sincronização de States Dependentes
Section titled “4. Sincronização de States Dependentes”const celsius = createState(0)const fahrenheit = createState(32)
const setCelsius = (value: number) => { batch(() => { celsius.set(value) fahrenheit.set((value * 9/5) + 32) })}Quando NÃO Usar
Section titled “Quando NÃO Usar”- Single update: Não há benefício em usar batch para um único
set() - Updates assíncronos:
batch()é síncrono, não funciona com async/await
// ❌ Batch não funciona aqui (assíncrono)batch(async () => { const data = await fetchData() state.set(data) // Executado APÓS batch finalizar})
// ✅ Corretoconst data = await fetchData()state.set(data)Performance Optimization
Section titled “Performance Optimization”Medindo Performance
Section titled “Medindo Performance”const state = createState(0)
state.watch(() => { // Simulando operação custosa console.time('render') // ... heavy DOM updates console.timeEnd('render')})
// Sem batch: 100 rendersconsole.time('without-batch')for (let i = 0; i < 100; i++) { state.set(i)}console.timeEnd('without-batch')
// Com batch: 1 renderconsole.time('with-batch')batch(() => { for (let i = 0; i < 100; i++) { state.set(i) }})console.timeEnd('with-batch')Resultados
Section titled “Resultados”Os ganhos dependem do custo dos watchers (e do DOM que eles tocam): meça no seu caso com o código acima.
Otimizações Automáticas
Section titled “Otimizações Automáticas”Slash já otimiza internamente:
- Deep Equality: não notifica se o valor não mudou
- Re-render por componente: só componentes que leram o state alterado executam de novo
Batch adiciona uma camada extra de otimização para cenários específicos.
Exemplos Práticos
Section titled “Exemplos Práticos”Exemplo 1: Form com Múltiplos Campos
Section titled “Exemplo 1: Form com Múltiplos Campos”import { createState, batch, html, render } from '@_bashell/slash'
interface FormData { name: string email: string age: number}
const form = createState<FormData>({ name: '', email: '', age: 0})
const resetForm = () => { batch(() => { form.set({ name: '', email: '', age: 0 }) })}
const loadUserData = (userId: number) => { // Simular fetch const userData = { name: 'Alice', email: 'alice@example.com', age: 30 }
batch(() => { form.set(userData) })}
// Binding reativo { get, subscribe }: o atributo `value` acompanha o state// sem recriar o <input> (e sem perder o foco)function field<K extends keyof FormData>(key: K) { return { get: () => form.get()[key], subscribe: (fn: (value: FormData[K]) => void) => form.watch((s) => fn(s[key])), }}
// Componente pequeno: só ele re-renderiza quando o form mudaconst Summary = () => html` <p>${form.get().name} / ${form.get().email} / ${form.get().age}</p>`
// O formulário em si não lê `form.get()` diretamenteconst FormComponent = () => html` <form> <input type="text" placeholder="Name" value=${field('name')} onInput=${(e: Event) => form.set({ ...form.get(), name: (e.target as HTMLInputElement).value }) } /> <input type="email" placeholder="Email" value=${field('email')} onInput=${(e: Event) => form.set({ ...form.get(), email: (e.target as HTMLInputElement).value }) } /> <input type="number" placeholder="Age" value=${field('age')} onInput=${(e: Event) => form.set({ ...form.get(), age: Number((e.target as HTMLInputElement).value) }) } /> <button type="button" onClick=${resetForm}>Reset</button> <button type="button" onClick=${() => loadUserData(1)}>Load User</button> <${Summary} /> </form>`
// Chamada direta: o form não vira dependência do state, então os inputs// mantêm o foco ao digitar. Reset e Load atualizam os campos via binding.render(FormComponent(), '#app')Exemplo 2: Lista com Filtros
Section titled “Exemplo 2: Lista com Filtros”import { createState, batch, html, render } from '@_bashell/slash'
interface Todo { id: number text: string completed: boolean}
const todos = createState<Todo[]>([])const filter = createState<'all' | 'active' | 'completed'>('all')const searchQuery = createState('')
const filteredTodos = createState<Todo[]>([])
// Recomputar filteredTodos quando qualquer dependência mudaconst updateFilteredTodos = () => { const allTodos = todos.get() const currentFilter = filter.get() const query = searchQuery.get().toLowerCase()
let result = allTodos
// Aplicar filtro if (currentFilter === 'active') { result = result.filter(t => !t.completed) } else if (currentFilter === 'completed') { result = result.filter(t => t.completed) }
// Aplicar busca if (query) { result = result.filter(t => t.text.toLowerCase().includes(query)) }
filteredTodos.set(result)}
// Watch all dependenciestodos.watch(updateFilteredTodos)filter.watch(updateFilteredTodos)searchQuery.watch(updateFilteredTodos)
const addTodo = (text: string) => { const newTodo: Todo = { id: Date.now(), text, completed: false }
batch(() => { todos.set([...todos.get(), newTodo]) // filteredTodos será atualizado automaticamente via watcher })}
const setFilter = (newFilter: 'all' | 'active' | 'completed') => { batch(() => { filter.set(newFilter) // filteredTodos será atualizado automaticamente })}
// A lista fica em um componente próprio: só ele lê `filteredTodos`const TodoList = () => html` <ul> ${filteredTodos.get().map(todo => html` <li>${todo.text}</li> `)} </ul>`
// TodoApp não lê nenhum state no render, então o <input> de busca// nunca é recriado e não perde o foco enquanto o usuário digitaconst TodoApp = () => html` <div> <input type="text" placeholder="Search..." onInput=${(e: Event) => searchQuery.set((e.target as HTMLInputElement).value)} /> <div> <button onClick=${() => setFilter('all')}>All</button> <button onClick=${() => setFilter('active')}>Active</button> <button onClick=${() => setFilter('completed')}>Completed</button> </div> <${TodoList} /> </div>`
render(html`<${TodoApp} />`, '#app')Exemplo 3: Data Fetching com Loading States
Section titled “Exemplo 3: Data Fetching com Loading States”import { createState, batch, html, render } from '@_bashell/slash'
interface User { id: number name: string email: string}
const user = createState<User | null>(null)const isLoading = createState(false)const error = createState<string | null>(null)
const fetchUser = async (id: number) => { batch(() => { isLoading.set(true) error.set(null) })
try { const response = await fetch(`https://api.example.com/users/${id}`) if (!response.ok) throw new Error('Failed to fetch')
const data = await response.json()
batch(() => { user.set(data) isLoading.set(false) }) } catch (err) { batch(() => { error.set((err as Error).message) isLoading.set(false) }) }}
const UserProfile = () => { const currentUser = user.get() const loading = isLoading.get() const errorMsg = error.get()
if (loading) { return html`<div>Loading...</div>` }
if (errorMsg) { return html`<div class="error">Error: ${errorMsg}</div>` }
if (!currentUser) { return html`<div>No user loaded</div>` }
return html` <div> <h1>${currentUser.name}</h1> <p>${currentUser.email}</p> </div> `}
render(html` <div> <${UserProfile} /> <button onClick=${() => fetchUser(1)}>Load User 1</button> </div>`, '#app')Exemplo 4: Animações com Múltiplos Estados
Section titled “Exemplo 4: Animações com Múltiplos Estados”import { createState, batch, html, render } from '@_bashell/slash'
const position = createState({ x: 0, y: 0 })const rotation = createState(0)const scale = createState(1)
const animateElement = () => { let frame = 0
const animate = () => { frame++
batch(() => { position.set({ x: Math.sin(frame * 0.05) * 100, y: Math.cos(frame * 0.05) * 100 }) rotation.set(frame * 2) scale.set(1 + Math.sin(frame * 0.1) * 0.5) })
requestAnimationFrame(animate) }
animate()}
const AnimatedElement = () => html` <div style=${{ position: 'absolute', left: '50%', top: '50%', transform: ` translate(${position.get().x}px, ${position.get().y}px) rotate(${rotation.get()}deg) scale(${scale.get()}) `, width: '50px', height: '50px', backgroundColor: 'blue' }} ></div>`
render(html`<${AnimatedElement} />`, '#app')animateElement()Integração com State Management
Section titled “Integração com State Management”set() respeita o batch
Section titled “set() respeita o batch”set() consulta o contexto de batch interno, então você não precisa passar nada para createState:
// state.ts (interno)const set = (payload: S) => { // ...
if (shouldNotifyWatchers(command)) { if (isInBatch()) { __enqueueBatchNotify(_notifyFinal) // Enfileira o notificador (deduplicado) } else { _notifyHandlers(deepClone(_state)) // Notifica imediatamente } }}Implementação: src/state.ts (trecho simplificado)
Verificar se Está em Batch
Section titled “Verificar se Está em Batch”isInBatch() existe em src/batch.ts, mas é de uso interno e não é exportado pelos subpaths públicos (core, router, forms, ssr) nem pelo bundle principal. Os subpaths exportam apenas batch.
Próximos Passos
Section titled “Próximos Passos”Agora que você domina batch updates, explore:
- Componentes - Criar componentes reutilizáveis
- Performance - Otimizações avançadas
- Router - Roteamento com state management