Guide et bonnes pratiques

Ce guide décrit les conventions de base, les modèles de conception de code et les flux de travail de refactorisation courants pour écrire des applications réactives déclaratives, propres et hautes performances avec @beforesemicolon/markup.


Règles de base

  1. Préférez les aides déclaratives aux branchements : évitez d'écrire des opérations ternaires JavaScript en ligne ou des fonctions impératives dans les chaînes du modèle HTML. Privilégiez toujours les helpers Markup intégrés (when, repeat, pick, is, etc.).
  2. Gardez les effets secondaires hors des fonctions de rendu : gardez le rendu du modèle strictement sans effets secondaires. Les effets secondaires doivent résider dans les blocs effect() ou les hooks de cycle de vie Web Component (tels que onMount).
  3. Immuabilité de l'état : ne mute pas directement les objets utilisés comme état canonique. Suivez les modifications dérivées/en attente séparément et fusionnez-les uniquement au moment du rendu.
  4. Préserver les collections sources : conservez les tableaux sources intacts lors de la dérivation de vues filtrées ou triées. La suppression d'un filtre ou d'une entrée de recherche devrait restaurer la liste complète des sources sans nécessiter de réinitialisation manuelle des données.
  5. Liaison de valeur réactive : liez directement les props et l'état (par exemple, disabled="${isDisabled}" au lieu de disabled="${isDisabled()}") pour permettre à Markup de configurer les écouteurs de manière chirurgicale.

Refactoriser les flux de travail

Voici comment migrer les habitudes impératives traditionnelles vers un code Markup propre et déclaratif.

1. Interface utilisateur conditionnelle (si/sinon)

Évitez d'écrire des conditions JavaScript ou des portes logiques en ligne dans les modèles.

Impératif/Éviter :

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

Déclaratif/Préfère :

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

Vous pouvez utiliser ternaire directement si vous avez l'intention d'effectuer un rendu une fois et/ou ne vous attendez pas à la mise à jour des données :

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

2. Listes de rendu

Évitez d'utiliser .map() dans des modèles pour générer des nœuds de liste dynamiques. L'utilisation de .map() détruit et reconstruit les nœuds à chaque mise à jour, tandis que repeat() utilise la mémorisation chirurgicale sous le capot.

Impératif/Éviter :

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

Déclaratif/Préfère :

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

Vous pouvez utiliser le map directement si vous avez l'intention d'effectuer un rendu une fois et/ou n'attendez pas de mises à jour :

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

3. Lectures facultatives imbriquées

Évitez d'utiliser le chaînage facultatif imbriqué (?.) directement dans les interpolations de l'interface utilisateur. Cela peut entraîner des erreurs d'exécution si des parties de la chaîne deviennent indéfinies ou non résolues.

Impératif/Éviter :

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

Déclaratif/Préfère :

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

L'option de sélection vous permet de définir des solutions de secours ou de gérer la valeur pour le formatage et/ou un traitement supplémentaire.

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

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

4. Composition d'expression booléenne

Évitez d'écrire des fonctions personnalisées qui combinent simplement plusieurs états avec && ou ||. Composez-les à l'aide des combinateurs booléens Markup.

Impératif/Éviter :

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

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

Déclaratif/Préfère :

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

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

Markup invite à la composition de fonctions et à l'utilisation de fonctions avec état. Vous devriez examiner davantage comment créer des assistants personnalisés pour mieux comprendre.


Modèles canoniques

Liste de recherche et de filtrage avec état

Il s'agit du modèle standard pour le rendu des collections avec filtrage dynamique. L'état source (items) reste totalement immuable.

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

Cela permet à l'état de rester immuable et vous permet de créer des états dérivés que vous utilisez pour le rendu qui combine différents états pour obtenir celui souhaité.

Machines à sous asynchrones (Suspense)

Utilisez suspense pour restituer proprement l'interface utilisateur asynchrone avec les gestionnaires de rendu d'erreur et de secours (pendant le chargement) :

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

Modèles plus courants

Voici des recettes plus typiques que vous pouvez copier-coller pour les exigences courantes de l'interface utilisateur :

Chèques d’adhésion et échange d’options

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 et styles réactifs 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>
`

Rendu de valeurs imbriquées

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

Magasin d'État partagé

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

Conventions et garde-corps

modifier ce document