Guia e melhores práticas
Este guia descreve as principais convenções, padrões de design de código e fluxos de trabalho de refatoração comuns para escrever aplicativos reativos declarativos, limpos e de alto desempenho com @beforesemicolon/markup.
Regras Básicas
- Prefira auxiliares declarativos em vez de ramificação: Evite escrever operações ternárias JavaScript in-line ou funções imperativas dentro de strings de modelo HTML. Sempre dê preferência aos auxiliares Markup integrados (
when,repeat,pick,is, etc.). - Mantenha os efeitos colaterais fora das funções de renderização: mantenha a renderização do modelo estritamente livre de efeitos colaterais. Os efeitos colaterais devem residir dentro de blocos
effect()ou ganchos de ciclo de vida Web Component (comoonMount). - Imutabilidade de estado: Não altere diretamente objetos usados como estado canônico. Acompanhe as alterações derivadas/pendentes separadamente e mescle-as somente no momento da renderização.
- Preservar coleções de origem: mantenha as matrizes de origem intactas ao derivar visualizações filtradas ou classificadas. A limpeza de um filtro ou entrada de pesquisa deve restaurar a lista completa de fontes sem exigir redefinições manuais de dados.
- Vinculação de valor reativo: Vincule props e estado diretamente (por exemplo,
disabled="${isDisabled}"em vez dedisabled="${isDisabled()}") para permitir que Markup configure ouvintes cirurgicamente.
Refatorar fluxos de trabalho
Veja como você pode migrar hábitos imperativos tradicionais para um código Markup limpo e declarativo.
1. UI condicional (if/else)
Evite escrever condições JavaScript inline ou portas lógicas dentro de modelos.
Imperativo/Evitar:
html`
<div>
${() => (isLoading() ? html`<p>Loading...</p>` : html`<p>Loaded!</p>`)}
</div>
`Declarativo/Preferencial:
html`
<div>${when(isLoading, html`<p>Loading...</p>`, html`<p>Loaded!</p>`)}</div>
`Você pode usar ternário diretamente se pretende renderizar uma vez e/ou não espera a atualização dos dados:
html` <div>${isLoading ? html`<p>Loading...</p>` : html`<p>Loaded!</p>`}</div> `2. Listas de renderização
Evite usar .map() dentro de modelos para gerar nós de lista dinâmica. O uso do .map() destrói e reconstrói nós em cada atualização, enquanto o repeat() usa memoização cirúrgica nos bastidores.
Imperativo/Evitar:
html`
<ul>
${() => items().map((item) => html`<li>${item.name}</li>`)}
</ul>
`Declarativo/Preferencial:
html`
<ul>
${repeat(items, (item) => html`<li>${item.name}</li>`)}
</ul>
`Você pode usar o map diretamente se pretende renderizar uma vez e ou não espera atualizações:
html`
<ul>
${list.map((item) => html`<li>${item.name}</li>`)}
</ul>
`3. Leituras opcionais aninhadas
Evite usar encadeamento opcional aninhado (?.) diretamente nas interpolações da IU. Isso pode levar a erros de tempo de execução se partes da cadeia ficarem indefinidas ou não resolvidas.
Imperativo/Evitar:
html`
<div>
<h2>${() => user()?.profile?.details?.name || 'Guest'}</h2>
</div>
`Declarativo/Preferencial:
html`
<div>
<h2>
${pick(user, 'profile.details.name', (name) => name || 'Guest')}
</h2>
</div>
`A opção de seleção permite definir substitutos ou manipular o valor para formatação e/ou processamento adicional.
const over18 = (age) => (age > 18 ? 'Over 18' : 'Under 18')
html`
<div>
<h2>${pick(user, 'profile.details.age', over18)}</h2>
</div>
`4. Composição de Expressão Booleana
Evite escrever funções personalizadas que apenas combinem vários estados com && ou ||. Componha-os usando combinadores booleanos Markup.
Imperativo/Evitar:
const canPublish = () => !isSaving() && hasChanges() && hasPermission()
html` <button disabled="${() => !canPublish()}">Publish</button> `Declarativo/Preferencial:
const canPublish = and(isNot(isSaving), is(hasChanges), is(hasPermission))
html` <button disabled="${isNot(canPublish)}">Publish</button> `Markup convida à composição de funções e ao trabalho com funções com estado. Você deve pesquisar mais sobre como criar auxiliares personalizados para entender mais.
Padrões Canônicos
Listagem de filtro e pesquisa com estado
Este é o padrão padrão para renderizar coleções com filtragem dinâmica. O estado de origem (items) permanece completamente imutável.
import { html, state, when, repeat, is, pick } from '@beforesemicolon/markup'
const [query, setQuery] = state('')
const [items] = state<Project[]>([])
// Derive filtered list reactively
const filtered = () =>
items().filter((p) => p.name.toLowerCase().includes(query().toLowerCase()))
const handleInput = (event: Event) => {
setQuery((event.target as HTMLInputElement).value)
}
const View = html`
<input value="${query}" oninput="${handleInput}" />
<ul>
${repeat(
filtered,
(item) => html`<li>${item.name}</li>`,
() => html`<p>No results found.</p>`
)}
</ul>
`Isso permite que o estado permaneça imutável e você crie estados derivados que você usa para renderização que combina diferentes estados para resolver o desejado.
Slots Assíncronos (Suspense)
Use suspense para renderizar a UI assíncrona de forma limpa com manipuladores de renderização de erro e substituto (durante o carregamento):
import { html, suspense } from '@beforesemicolon/markup'
const resource = async () => {
const res = await fetch('/api/data')
const data = await res.json()
return html`<p>Resolved: ${data.message}</p>`
}
const ResourceView = html`
${suspense(
resource,
html`<p>Loading resource...</p>`,
(err) => html`<p class="error">Error: ${err.message}</p>`
)}
`Padrões mais comuns
Aqui estão receitas mais típicas que você pode copiar e colar para requisitos comuns de IU:
Verificações de associação e troca de opções
import { html, state, when, oneOf } from '@beforesemicolon/markup'
const [mode, setMode] = state<'view' | 'edit' | 'preview'>('view')
const View = html`
${when(
oneOf(mode, ['edit', 'preview']),
html`<button onclick="${() => setMode('view')}">Done</button>`,
html`<button onclick="${() => setMode('edit')}">Edit</button>`
)}
`Variáveis e estilos reativos CSS
import { html, state } from '@beforesemicolon/markup'
const [gap] = state(12)
// Reactive style bindings cleared and updated dynamically
const Box = html`
<div style="--gap: ${() => `${gap()}px`}; margin: ${gap}px">
Spacing Gap: ${gap}px
</div>
`Renderização de valor aninhado
import { html, state, pick } from '@beforesemicolon/markup'
const [currentEntity] = state({ details: { author: { name: 'Ada Lovelace' } } })
// Safe nested navigation via pick
const AuthorHeader = html`
<h1>Written by: ${pick(currentEntity, 'details.author.name')}</h1>
`Armazenamento de Estado Compartilhado
import { state } from '@beforesemicolon/markup'
export const [todos, setTodos] = state<Todo[]>([])
export const [loadingState, setLoadingState] = state<
'idle' | 'loading' | 'error'
>('idle')
export const fetchTodos = async () => {
setLoadingState('loading')
try {
const response = await fetch('/api/todos')
const list = await response.json()
setTodos(list)
setLoadingState('idle')
} catch {
setLoadingState('error')
}
}Convenções e guarda-corpos
- Passe getters/funções diretamente: não execute getters dentro de atributos de modelo quando se pretende assinatura/reatividade. Passe
disabled="${isDisabled}", nãodisabled="${isDisabled()}". - Limpar vinculações de eventos: não envolva retornos de chamada em fechamentos redundantes, a menos que passe argumentos:
- Bom:
<button onclick="${logout}">Logout</button> - Bom:
<button onclick="${() => handleSelect(item)}">Select</button> - Evitar:
<button onclick="${() => logout()}">Logout</button>
- Bom:
- Associações diretas de propriedades: Não pré-normalize atributos de modelo simples em setup/getters apenas para renderizá-los. Markup manipula valores de atributos
undefined, vazios e falsos corretamente. - Atributos booleanos: o núcleo Markup desembrulha e avalia automaticamente os estados booleanos. Não adicione wrappers estilo
Boolean(val(this.props.someBoolean())); vincular propriedades diretamente. - Estático vs. Reativo: se um valor for estático (nunca muda após a inicialização), renderize seu estado avaliado:
html${state()}
. If it is reactive and should dynamically update, bind the getter:html<p>${state}</p>.