Múltiplas Atualizações
Quando você atualiza o mesmo estado várias vezes em sequência
Este guia apresenta técnicas de otimização e best practices para construir aplicações Slash performáticas e escaláveis.
batch()O batch() agrupa múltiplas atualizações de estado, notificando watchers apenas uma vez ao final.
Múltiplas Atualizações
Quando você atualiza o mesmo estado várias vezes em sequência
Estados Relacionados
Quando atualiza múltiplos estados que dependem uns dos outros
Operações em Loop
Quando processa arrays/listas e atualiza estado para cada item
import { createState, batch } from '@_bashell/slash';
const state = createState({ count: 0, name: 'John', active: false });
// ✅ BOM: Múltiplas atualizações agrupadasbatch(() => { state.set({ ...state.get(), count: 1 }); state.set({ ...state.get(), name: 'Jane' }); state.set({ ...state.get(), active: true });});// Watchers notificados apenas 1x
// ✅ BOM: Processamento de listaconst items = [1, 2, 3, 4, 5];const listState = createState({ processed: [] });
batch(() => { items.forEach(item => { const current = listState.get(); listState.set({ processed: [...current.processed, item * 2] }); });});// ❌ RUIM: Apenas uma atualizaçãobatch(() => { state.set({ count: 1 });});// Overhead desnecessário
// ❌ RUIM: Batch aninhado (redundante)batch(() => { batch(() => { // Warning emitido! state.set({ count: 1 }); });});
// ❌ RUIM: Com operações assíncronasbatch(() => { state.set({ loading: true });
// ❌ Erro: fetchData é async, batch não aguarda fetchData().then(data => { state.set({ data, loading: false }); });});
// ✅ CORRETO: Batch síncrono, async forastate.set({ loading: true });
fetchData().then(data => { batch(() => { state.set({ data, loading: false }); });});import { createState, batch } from '@_bashell/slash';
const state = createState({ a: 0, b: 0, c: 0 });
let renderCount = 0;state.watch(() => { renderCount++;});
// Sem batch: 3 rendersstate.set({ a: 1, b: 0, c: 0 });state.set({ a: 1, b: 2, c: 0 });state.set({ a: 1, b: 2, c: 3 });console.log(renderCount); // 3
// Com batch: 1 renderrenderCount = 0;batch(() => { state.set({ a: 1, b: 0, c: 0 }); state.set({ a: 1, b: 2, c: 0 }); state.set({ a: 1, b: 2, c: 3 });});console.log(renderCount); // 1 ✅import { html, createState } from '@_bashell/slash';
const count = createState(0);
// ❌ RUIM: Cria nova função a cada renderfunction Counter() { return html` <button onClick=${() => count.set(count.get() + 1)}> Count: ${count.get()} </button> `;}
// ✅ BOM: Função reutilizávelconst increment = () => count.set(count.get() + 1);
function Counter() { return html` <button onClick=${increment}> Count: ${count.get()} </button> `;}import { createState } from '@_bashell/slash';
const state = createState({ users: [], filters: {} });
// ❌ RUIM: Recalcula semprefunction getFilteredUsers() { const { users, filters } = state.get(); return users.filter(user => { // Computação pesada return matchesFilters(user, filters); });}
// ✅ BOM: Cacheia resultadolet cachedFilters = null;let cachedResult = null;
function getFilteredUsers() { const { users, filters } = state.get();
// Só recalcula se filtros mudaram if (filters !== cachedFilters) { cachedFilters = filters; cachedResult = users.filter(user => matchesFilters(user, filters)); }
return cachedResult;}// ❌ RUIM: Estado monolíticoconst appState = createState({ user: { id: 1, name: 'John', email: '...' }, settings: { theme: 'dark', lang: 'en', ... }, cart: { items: [...], total: 0 }, notifications: { list: [...], unread: 0 }, // ... 50+ propriedades});
// Qualquer mudança notifica TODOS os watchers
// ✅ BOM: Estados separados por domínioconst userState = createState({ id: 1, name: 'John', email: '...' });const settingsState = createState({ theme: 'dark', lang: 'en' });const cartState = createState({ items: [], total: 0 });const notificationsState = createState({ list: [], unread: 0 });
// Mudanças isoladas, watchers específicosimport { html, createState } from '@_bashell/slash';
const globalState = createState({ data: [] });
// ❌ RUIM: Watcher sem cleanup (memory leak)function Component() { globalState.watch((state) => { console.log('State changed:', state); }); // Se Component() for chamado múltiplas vezes, // acumula watchers que nunca são removidos!
return html`<div>...</div>`;}
// ✅ BOM: crie o watcher uma vez, fora do componente, e guarde o unwatchconst unwatch = globalState.watch((state) => { console.log('State changed:', state);});
function Component() { return html`<div>...</div>`;}
// Quando não precisar mais:unwatch();// ❌ RUIM: Map comum (memory leak)const cache = new Map<string, any>();
function cacheData(key: string, data: any) { cache.set(key, data);}// Dados nunca são garbage collected
// ✅ BOM: WeakMap para cacheconst cache = new WeakMap<object, any>();
function cacheData(obj: object, data: any) { cache.set(obj, data);}// Quando obj não tem mais referências, é GC automaticamenterender() já destrói os nós anteriores do container antes de renderizar. Só chame destroyNode() quando você remover nós criados pelo Slash por conta própria:
import { destroyNode } from '@_bashell/slash';
const node = document.querySelector('#widget')!;
destroyNode(node); // executa as limpezas do Slash (listeners, inscrições)node.remove();Sem API pública de cleanup por nó, o padrão é manter o recurso fora do componente e encerrá-lo explicitamente.
import { html, createState, render } from '@_bashell/slash';
const time = createState(0);let interval: ReturnType<typeof setInterval> | null = null;
const start = () => { if (!interval) interval = setInterval(() => time.set(time.get() + 1), 1000);};
const stop = () => { if (interval) clearInterval(interval); interval = null;};
function Timer() { return html` <div> <p>Time: ${time.get()}</p> <button onClick=${start}>Start</button> <button onClick=${stop}>Stop</button> </div> `;}
render(html`<${Timer} />`, '#app');const handler = () => console.log('Window resized:', window.innerWidth);
// Registre e remova em código que você controla (não no corpo do componente)export const startTracking = () => window.addEventListener('resize', handler);export const stopTracking = () => window.removeEventListener('resize', handler);let ws: WebSocket | null = null;
export const connect = () => { ws = new WebSocket('ws://localhost:8080'); ws.onmessage = (event) => console.log('Message:', event.data);};
export const disconnect = () => { ws?.close(); ws = null;};import { createState } from '@_bashell/slash';
// ❌ RUIM: Clonar objeto grande toda horaconst state = createState({ hugeArray: new Array(10000).fill({}), config: { /* ... */ }});
// Clona tudo a cada set()state.set({ ...state.get(), config: { updated: true }});
// ✅ BOM: Separe dados grandesconst dataState = createState({ hugeArray: new Array(10000).fill({})});
const configState = createState({ config: { updated: false }});
// Apenas config é clonadoconfigState.set({ config: { updated: true } });import { createState } from '@_bashell/slash';
const state = createState({ count: 0 });
// ❌ RUIM: Watcher que só logastate.watch((s) => { console.log('State:', s);});
// ✅ BOM: Use apenas quando necessário para UIstate.watch((s) => { updateUI(s);});import { html } from '@_bashell/slash';
// ❌ RUIM: Importa tudo upfrontimport ComponentA from './a';import ComponentB from './b';import ComponentC from './c';
// ✅ BOM: Dynamic importasync function App() { const route = getCurrentRoute();
if (route === '/a') { const { ComponentA } = await import('./a'); return html`<${ComponentA} />`; } else if (route === '/b') { const { ComponentB } = await import('./b'); return html`<${ComponentB} />`; }}import { html, createState } from '@_bashell/slash';
const searchQuery = createState('');const results = createState([]);
// ❌ RUIM: Busca a cada teclafunction handleInput(e: Event) { const value = (e.target as HTMLInputElement).value; searchQuery.set(value);
// API call a cada keystroke! fetchResults(value).then(data => { results.set(data); });}
// ✅ BOM: Debouncelet debounceTimer: number;
function handleInput(e: Event) { const value = (e.target as HTMLInputElement).value; searchQuery.set(value);
clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { fetchResults(value).then(data => { results.set(data); }); }, 300);}import { html, createState } from '@_bashell/slash';
const items = createState(new Array(10000).fill(null).map((_, i) => ({ id: i, name: `Item ${i}` })));
// ❌ RUIM: Renderiza 10k itensfunction List() { return html` <div> ${items.get().map(item => html` <div key=${item.id}>${item.name}</div> `)} </div> `;}
// ✅ BOM: Renderiza apenas itens visíveisconst visibleRange = createState({ start: 0, end: 50 });
function VirtualList() { const visible = items.get().slice( visibleRange.get().start, visibleRange.get().end );
return html` <div onscroll=${handleScroll}> ${visible.map(item => html` <div key=${item.id}>${item.name}</div> `)} </div> `;}batch()import { createState } from '@_bashell/slash';
const state = createState({ count: 0 });
const start = performance.now();
state.watch((s) => { console.log('Watcher executado');});
state.set({ count: 1 });
const end = performance.now();console.log(`Tempo de notificação: ${end - start}ms`);import { createState, batch } from '@_bashell/slash';
const state = createState({ a: 0, b: 0, c: 0 });
// Sem batchconst start1 = performance.now();state.set({ a: 1, b: 0, c: 0 });state.set({ a: 1, b: 2, c: 0 });state.set({ a: 1, b: 2, c: 3 });const end1 = performance.now();
// Com batchconst start2 = performance.now();batch(() => { state.set({ a: 1, b: 0, c: 0 }); state.set({ a: 1, b: 2, c: 0 }); state.set({ a: 1, b: 2, c: 3 });});const end2 = performance.now();
console.log(`Sem batch: ${end1 - start1}ms`);console.log(`Com batch: ${end2 - start2}ms`);console.log(`Melhoria: ${((end1 - start1) / (end2 - start2)).toFixed(2)}x`);