Sistema de Estado
createState() - Criação de Estado Reativo
Section titled “createState() - Criação de Estado Reativo”A função createState() cria um container de estado observável: watchers são notificados quando o valor muda e componentes que leem o estado re-renderizam automaticamente.
Assinatura
Section titled “Assinatura”function createState<T>( initialValue: T, options?: StateOptions): State<T>
interface StateOptions { enableHistory?: boolean // Habilita time-travel debugging historyMaxSize?: number // Tamanho máximo do histórico (padrão: 100)}
interface State<T> { get(): T set(value: T): void watch(callback: (value: T) => void): () => void // Opcionais (apenas se enableHistory: true) getHistory?(): Readonly<StateHistory<T>> clearHistory?(): void}Uso Básico
Section titled “Uso Básico”import { createState } from '@_bashell/slash'
// Estado simplesconst count = createState(0)
console.log(count.get()) // 0count.set(5)console.log(count.get()) // 5Estado com Objetos
Section titled “Estado com Objetos”interface User { name: string age: number}
const user = createState<User>({ name: 'Alice', age: 30})
// Atualizar objeto completouser.set({ name: 'Bob', age: 25 })
// Atualizar parcialmente (spread)user.set({ ...user.get(), age: 31 })Estado com Arrays
Section titled “Estado com Arrays”const todos = createState<string[]>([ 'Buy milk', 'Walk dog'])
// Adicionar itemtodos.set([...todos.get(), 'Learn Slash'])
// Remover itemtodos.set(todos.get().filter(todo => todo !== 'Buy milk'))
// Atualizar itemtodos.set( todos.get().map((todo, i) => i === 0 ? 'Buy bread' : todo ))Implementação: src/state.ts
Métodos: get(), set(), watch()
Section titled “Métodos: get(), set(), watch()”get() - Obter Valor Atual
Section titled “get() - Obter Valor Atual”Retorna um clone profundo do estado atual:
const state = createState({ count: 0 })
const value1 = state.get()const value2 = state.get()
console.log(value1 === value2) // false (diferentes clones)console.log(value1.count === value2.count) // true (valores iguais)Por que clone?
- Previne mutações acidentais
- Garante imutabilidade
- Facilita debugging e time-travel
SSR Tracking:
Em modo SSR, get() retorna um Proxy que rastreia acessos a propriedades para otimizar serialização.
set() - Atualizar Valor
Section titled “set() - Atualizar Valor”Atualiza o estado e notifica watchers automaticamente:
const count = createState(0)
count.set(5) // Atualiza para 5count.set(10) // Atualiza para 10count.set(count.get() + 1) // IncrementaComportamento:
- Deep Clone: Novo valor é clonado profundamente
- Comparação: Compara com valor anterior (deep equal)
- Notificação: Watchers são notificados apenas se o valor mudou
- Batching: Se dentro de
batch(), notificações são agrupadas
Nota: set() sempre substitui o valor completo. Para atualizações parciais, use spread:
const user = createState({ name: 'Alice', age: 30 })
// ❌ Errado - sobrescreve objetouser.set({ age: 31 })
// ✅ Correto - preserva outras propsuser.set({ ...user.get(), age: 31 })watch() - Observar Mudanças
Section titled “watch() - Observar Mudanças”Registra callback para ser notificado quando o estado muda:
const count = createState(0)
const unwatch = count.watch((newValue) => { console.log('Count changed to:', newValue)})
count.set(5) // Log: "Count changed to: 5"count.set(10) // Log: "Count changed to: 10"
// Parar de observarunwatch()
count.set(15) // Sem log (unwatched)Assinatura:
watch(callback: (newValue: T) => void): () => voidRetorno: Função unwatch para remover o callback
Características:
- Callback recebe clone do novo valor
- Múltiplos watchers podem ser registrados
- Watchers são notificados na ordem de registro
- Não há notificação se valor não mudou (deep equal)
- O último valor vence: se um watcher chamar
set()no mesmo estado durante a notificação, a notificação aninhada entrega o valor atual a todos os watchers e a notificação antiga é interrompida.- Nenhum watcher recebe um valor velho depois do novo: o último valor recebido por qualquer watcher é sempre igual a
get()ao fim dosetmais externo. - Watchers anteriores ao que fez o
setveem o valor antigo e depois o novo. setsegue síncrono, e um erro lançado na notificação aninhada chega a quem chamouset.
- Nenhum watcher recebe um valor velho depois do novo: o último valor recebido por qualquer watcher é sempre igual a
Reatividade Automática
Section titled “Reatividade Automática”Componentes que leem um state durante a renderização se inscrevem automaticamente nele e re-renderizam quando ele muda. O padrão é observer: o rastreamento é feito pelas chamadas a get() feitas enquanto o componente executa.
Como Funciona
Section titled “Como Funciona”import { html, createState, render } from '@_bashell/slash'
// O state fica fora do componente: se fosse criado dentro, seria recriado a cada renderconst count = createState(0)
const Counter = () => html` <div> <p>Count: ${count.get()}</p> <button onClick=${() => count.set(count.get() + 1)}> Increment </button> </div>`
// Monte o componente como <${Counter} />. render(Counter(), '#app') renderiza uma vez, sem reatividaderender(html`<${Counter} />`, '#app')O que acontece:
- Ao montar
<${Counter} />, o componente executa e cadacount.get()registracountcomo dependência dele - Quando
count.set()muda o valor (deep equal),countnotifica seus watchers - O componente executa de novo e seus nós anteriores são substituídos pelos novos
get()chamado dentro de um event handler (fora da execução do componente) não cria dependência- A cada execução o componente refaz o rastreamento: passa a observar os states recém-lidos (por exemplo, depois de um
if (loading.get()) return ...) e deixa de observar os que não leu mais. Exceção: um componente que não leu nenhum state na primeira execução é estático e nunca re-executa
A granularidade é o componente, não o nó: não há atualização de um único <p>. Divida a interface em componentes pequenos para que uma mudança re-renderize só o necessário.
Tracking de Estados
Section titled “Tracking de Estados”Slash rastreia quais states um componente leu durante a renderização:
const name = createState('Alice')const age = createState(30)
const Profile = () => html` <div> <h1>${name.get()}</h1> <p>Age: ${age.get()}</p> </div>`name.set('Bob')ouage.set(31)re-renderizamProfile, porque ele leu os dois- Um componente que leu só
namenão re-renderiza quandoagemuda
Implementação: src/rendering/element-core.ts
State em Props
Section titled “State em Props”Valores lidos com get() também funcionam em props, e o componente re-renderiza quando o state muda:
const isActive = createState(false)
const Button = () => html` <button class=${isActive.get() ? 'active' : 'inactive'}> Toggle </button>`
// Quando isActive muda, o componente Button re-renderiza com a nova classisActive.set(true)State em Arrays
Section titled “State em Arrays”const items = createState([1, 2, 3])
const List = () => html` <ul> ${items.get().map(item => html`<li>${item}</li>`)} </ul>`
// Quando items muda, o componente é re-renderizadoitems.set([...items.get(), 4])Nota: Para listas longas, considere técnicas de virtualização ou memoização.
Objetos Reactive
Section titled “Objetos Reactive”Um State tem get/watch, mas não subscribe, então passar o próprio state como child ou prop (${count}) não é reativo: use ${count.get()} dentro de um componente. Objetos que implementam Reactive<T> (get() + subscribe(fn)) são aceitos como child ou prop e mantidos em sincronia por subscribe. É o caso de Router({ router }) e dos controles de formulário (textFieldControl etc.).
Deep Cloning e Imutabilidade
Section titled “Deep Cloning e Imutabilidade”Por que Imutabilidade?
Section titled “Por que Imutabilidade?”Slash adota imutabilidade para:
- Previsibilidade: Estado nunca muda “por baixo dos panos”
- Debugging: Fácil rastrear mudanças
- Time-travel: Histórico de estados é possível
- Detecção de mudança: o novo valor é comparado em profundidade (deep equal) com o anterior
Deep Clone Automático
Section titled “Deep Clone Automático”createState() clona profundamente valores em:
set(): Valor passado é clonado antes de armazenarget(): Valor retornado é um clone (não o original)
const state = createState({ user: { name: 'Alice' } })
const obj1 = state.get()obj1.user.name = 'Bob' // Mutação local (não afeta state)
console.log(state.get().user.name) // 'Alice' (state não mudou)Implementação do Deep Clone
Section titled “Implementação do Deep Clone”Functional Core: src/state-core.ts
// Simplified versionfunction deepClone<T>(value: T): T { // Primitives if (value === null || typeof value !== 'object') { return value }
// Error é preservado (mesma instância); Date vira uma nova instância if (value instanceof Error) return value if (value instanceof Date) return new Date(value.getTime()) as T
// Arrays if (Array.isArray(value)) { return value.map(deepClone) as unknown as T }
// Objects const cloned = {} as T for (const key in value) { if (value.hasOwnProperty(key)) { cloned[key] = deepClone(value[key]) } } return cloned}Otimizações:
- Tratamento especial para
Error(preservado) eDate(nova instância) Map,Set,RegExp, funções e Symbols não são suportados como valores de state (não são clonados corretamente)- O
deepEqualusa um cache emWeakMappara acelerar comparações repetidas
Deep Equality
Section titled “Deep Equality”Slash compara valores profundamente para decidir se deve notificar watchers:
const state = createState({ count: 0 })
state.watch(() => console.log('Changed!'))
state.set({ count: 0 }) // Sem log (valor igual ao anterior)state.set({ count: 1 }) // Log: "Changed!" (valor diferente)Implementação: src/state-core.ts
// Simplified versionfunction deepEqual<T>(a: T, b: T): boolean { if (a === b) return true if (typeof a !== 'object' || typeof b !== 'object') return false if (a === null || b === null) return false
const keysA = Object.keys(a) const keysB = Object.keys(b)
if (keysA.length !== keysB.length) return false
return keysA.every(key => deepEqual((a as any)[key], (b as any)[key]) )}State Options (History/Time-Travel Debugging)
Section titled “State Options (History/Time-Travel Debugging)”Habilitando Histórico
Section titled “Habilitando Histórico”const count = createState(0, { enableHistory: true })
count.set(1)count.set(2)count.set(3)
const history = count.getHistory!()console.log(history.entries.length) // 3getHistory() - Obter Histórico
Section titled “getHistory() - Obter Histórico”Retorna histórico de comandos e estados resultantes. Um set com valor igual ao atual também gera uma entrada, com command: { type: 'NO_CHANGE' }:
interface StateHistory<T> { entries: ReadonlyArray<HistoryEntry<T>> maxSize: number}
interface HistoryEntry<T> { timestamp: number command: StateCommand<T> resultingState: T}Exemplo:
const count = createState(0, { enableHistory: true })
count.set(5)count.set(10)
const history = count.getHistory!()
for (const entry of history.entries) { console.log({ time: new Date(entry.timestamp), command: entry.command, result: entry.resultingState })}Output:
{ time: 2026-02-03T10:30:45.123Z, command: { type: 'UPDATE', oldState: 0, newState: 5 }, result: 5}{ time: 2026-02-03T10:30:46.456Z, command: { type: 'UPDATE', oldState: 5, newState: 10 }, result: 10}clearHistory() - Limpar Histórico
Section titled “clearHistory() - Limpar Histórico”Remove todos os entries do histórico:
const state = createState(0, { enableHistory: true })
state.set(1)state.set(2)state.set(3)
console.log(state.getHistory!().entries.length) // 3
state.clearHistory!()
console.log(state.getHistory!().entries.length) // 0Configurando Tamanho Máximo
Section titled “Configurando Tamanho Máximo”Limite o número de entries mantidos no histórico:
const state = createState(0, { enableHistory: true, historyMaxSize: 50 // Mantém apenas últimos 50 comandos})
// Após 100 comandos, apenas últimos 50 são mantidosfor (let i = 0; i < 100; i++) { state.set(i)}
console.log(state.getHistory!().entries.length) // 50Comportamento: FIFO (First-In-First-Out) - comandos mais antigos são removidos primeiro.
Use Cases para Time-Travel
Section titled “Use Cases para Time-Travel”- Debugging: Inspecionar sequência de mudanças
- Undo/Redo: Implementar funcionalidade de desfazer
- Auditoria: Rastrear alterações em dados críticos
- Replay: Reproduzir sequência de ações
Exemplo - Undo/Redo:
const editor = createState('', { enableHistory: true })
const undo = () => { const history = editor.getHistory!() const entries = history.entries
if (entries.length > 1) { const previous = entries[entries.length - 2] editor.set(previous.resultingState) }}
editor.set('Hello')editor.set('Hello World')editor.set('Hello World!')
console.log(editor.get()) // 'Hello World!'undo()console.log(editor.get()) // 'Hello World'Implementação: src/state-history.ts
Performance Considerations
Section titled “Performance Considerations”Time-travel tem overhead de memória. Use apenas quando necessário:
- Desenvolvimento: Habilite para debugging
- Produção: Desabilite para apps com muitos states
- Seletivo: Habilite apenas em states críticos
// Dev modeconst isDevMode = process.env.NODE_ENV !== 'production'
const state = createState(initialValue, { enableHistory: isDevMode})Exemplos Práticos
Section titled “Exemplos Práticos”Exemplo 1: Counter com Watch
Section titled “Exemplo 1: Counter com Watch”import { createState, html, render } from '@_bashell/slash'
const count = createState(0)
// Log todas as mudançascount.watch((newValue) => { console.log(`Count changed to: ${newValue}`)})
const Counter = () => html` <div> <p>Count: ${count.get()}</p> <button onClick=${() => count.set(count.get() + 1)}>+</button> <button onClick=${() => count.set(count.get() - 1)}>-</button> <button onClick=${() => count.set(0)}>Reset</button> </div>`
render(html`<${Counter} />`, '#app')Exemplo 2: Todo List com Estado Complexo
Section titled “Exemplo 2: Todo List com Estado Complexo”import { createState, html, render } from '@_bashell/slash'
interface Todo { id: number text: string completed: boolean}
const todos = createState<Todo[]>([])
// Texto em edição fora de qualquer state lido no render: o <input> não é recriado a cada teclalet draft = ''
const addTodo = () => { const text = draft.trim() if (!text) return
const newTodo: Todo = { id: Date.now(), text, completed: false }
draft = '' todos.set([...todos.get(), newTodo])}
const toggleTodo = (id: number) => { todos.set( todos.get().map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) )}
const TodoApp = () => html` <div> <h1>Todos</h1> <input type="text" onInput=${(e: Event) => { draft = (e.target as HTMLInputElement).value }} onKeypress=${(e: KeyboardEvent) => e.key === 'Enter' && addTodo()} /> <button onClick=${addTodo}>Add</button> <ul> ${todos.get().map(todo => html` <li style=${{ textDecoration: todo.completed ? 'line-through' : 'none' }} onClick=${() => toggleTodo(todo.id)} > ${todo.text} </li> `)} </ul> </div>`
render(html`<${TodoApp} />`, '#app')Exemplo 3: Form com Validação
Section titled “Exemplo 3: Form com Validação”import { createState, html, render } from '@_bashell/slash'
interface FormData { email: string password: string}
interface FormErrors { email?: string password?: string}
const form = createState<FormData>({ email: '', password: '' })const errors = createState<FormErrors>({})
const validate = (): boolean => { const data = form.get() const newErrors: FormErrors = {}
if (!data.email.includes('@')) { newErrors.email = 'Invalid email' }
if (data.password.length < 6) { newErrors.password = 'Password must be at least 6 characters' }
errors.set(newErrors) return Object.keys(newErrors).length === 0}
const handleSubmit = (e: Event) => { e.preventDefault() if (validate()) { console.log('Form submitted:', form.get()) }}
// Só este componente lê `errors`: os inputs não são recriados ao validarconst FieldErrors = () => { const { email, password } = errors.get() return html` <div> ${email && html`<p class="error">${email}</p>`} ${password && html`<p class="error">${password}</p>`} </div> `}
// `form` só é lido dentro dos handlers, então o form não re-renderiza a cada teclaconst LoginForm = () => html` <form onSubmit=${handleSubmit}> <input type="email" placeholder="Email" onInput=${(e: Event) => form.set({ ...form.get(), email: (e.target as HTMLInputElement).value }) } /> <input type="password" placeholder="Password" onInput=${(e: Event) => form.set({ ...form.get(), password: (e.target as HTMLInputElement).value }) } /> <${FieldErrors} /> <button type="submit">Login</button> </form>`
render(html`<${LoginForm} />`, '#app')Próximos Passos
Section titled “Próximos Passos”Agora que você domina o sistema de estado, explore:
- Batch Updates - Otimizar múltiplas atualizações de estado
- Componentes - Usar state em componentes reutilizáveis
- Router - State management para roteamento