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

  1. 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.).
  2. 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 (como onMount).
  3. 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.
  4. 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.
  5. Vinculação de valor reativo: Vincule props e estado diretamente (por exemplo, disabled="${isDisabled}" em vez de disabled="${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:

javascript
html`
    <div>
        ${() => (isLoading() ? html`<p>Loading...</p>` : html`<p>Loaded!</p>`)}
    </div>
`

Declarativo/Preferencial:

javascript
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:

javascript
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:

javascript
html`
    <ul>
        ${() => items().map((item) => html`<li>${item.name}</li>`)}
    </ul>
`

Declarativo/Preferencial:

javascript
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:

javascript
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:

javascript
html`
    <div>
        <h2>${() => user()?.profile?.details?.name || 'Guest'}</h2>
    </div>
`

Declarativo/Preferencial:

javascript
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.

javascript
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:

javascript
const canPublish = () => !isSaving() && hasChanges() && hasPermission()

html` <button disabled="${() => !canPublish()}">Publish</button> `

Declarativo/Preferencial:

javascript
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.

typescript
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):

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

editar este documento