Guía y mejores prácticas

Esta guía describe las convenciones principales, los patrones de diseño de código y los flujos de trabajo de refactorización comunes para escribir aplicaciones reactivas declarativas, limpias y de alto rendimiento con @beforesemicolon/markup.


Reglas básicas

  1. Prefiera ayudas declarativas a bifurcaciones: evite escribir operaciones ternarias JavaScript en línea o funciones imperativas dentro de cadenas de plantilla HTML. Favorezca siempre los asistentes Markup integrados (when, repeat, pick, is, etc.).
  2. Mantenga los efectos secundarios fuera de las funciones de renderizado: mantenga el renderizado de la plantilla estrictamente libre de efectos secundarios. Los efectos secundarios deben vivir dentro de los bloques effect() o ganchos del ciclo de vida Web Component (como onMount).
  3. Inmutabilidad del estado: No mute los objetos utilizados como estado canónico directamente. Realice un seguimiento de los cambios derivados/pendientes por separado y combínelos solo en el momento del renderizado.
  4. Conservar colecciones de origen: mantenga intactas las matrices de origen al derivar vistas filtradas u ordenadas. Borrar un filtro o una entrada de búsqueda debería restaurar la lista de fuentes completa sin necesidad de restablecer manualmente los datos.
  5. Enlace de valor reactivo: vincule props y estados directamente (por ejemplo, disabled="${isDisabled}" en lugar de disabled="${isDisabled()}") para permitir que Markup configure los oyentes quirúrgicamente.

Refactorizar flujos de trabajo

Así es como puede migrar los hábitos imperativos tradicionales a un código Markup limpio y declarativo.

1. UI condicional (si/si no)

Evite escribir condiciones JavaScript en línea o puertas lógicas dentro de las plantillas.

Imperativo/Evitar:

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

Declarativo/Preferido:

javascript
html`
    <div>${when(isLoading, html`<p>Loading...</p>`, html`<p>Loaded!</p>`)}</div>
`

Puede usar ternario directamente si tiene la intención de renderizar una vez o no espera la actualización de datos:

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

2. Listas de renderizado

Evite el uso de .map() dentro de plantillas para generar nodos de lista dinámica. El uso de .map() destruye y reconstruye nodos en cada actualización, mientras que repeat() utiliza memorización quirúrgica bajo el capó.

Imperativo/Evitar:

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

Declarativo/Preferido:

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

Puede usar map directamente si desea renderizar una vez o no espera actualizaciones:

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

3. Lecturas opcionales anidadas

Evite el uso de encadenamiento opcional anidado (?.) directamente dentro de las interpolaciones de la interfaz de usuario. Esto puede provocar errores de tiempo de ejecución si partes de la cadena quedan indefinidas o sin resolver.

Imperativo/Evitar:

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

Declarativo/Preferido:

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

La opción de selección le permite definir alternativas o manejar el valor para formatear o procesar adicionalmente.

javascript
const over18 = (age) => (age > 18 ? 'Over 18' : 'Under 18')

html`
    <div>
        <h2>${pick(user, 'profile.details.age', over18)}</h2>
    </div>
`

4. Composición de expresiones booleanas

Evite escribir funciones personalizadas que simplemente combinen múltiples estados con && o ||. Compóngalos utilizando combinadores booleanos Markup.

Imperativo/Evitar:

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

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

Declarativo/Preferido:

javascript
const canPublish = and(isNot(isSaving), is(hasChanges), is(hasPermission))

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

Markup invita a la composición de funciones y al trabajo con funciones con estado. Debería investigar más sobre cómo crear ayudas personalizadas para comprender más.


Patrones canónicos

Búsqueda con estado y listado de filtros

Este es el patrón estándar para representar colecciones con filtrado dinámico. El estado de origen (items) permanece completamente inmutable.

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>
`

Esto permite que el estado permanezca inmutable y que usted pueda crear estados derivados que utilice para renderizar y que combinen diferentes estados para resolver el deseado.

Ranuras asíncronas (suspenso)

Utilice suspense para representar la interfaz de usuario asíncrona limpiamente con errores y controladores de representación alternativos (durante la carga):

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>`
    )}
`

Patrones más comunes

Aquí hay recetas más típicas que puede copiar y pegar para requisitos comunes de la interfaz de usuario:

Cheques de membresía e intercambio de opciones

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>`
    )}
`

Variables y estilos reactivos 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>
`

Representación de valores anidados

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>
`

Tienda estatal compartida

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')
    }
}

Convenciones y barandillas

editar este documento