Guide & Best Practices

This guide outlines core conventions, code design patterns, and common refactoring workflows for writing declarative, clean, and high-performance reactive applications with @beforesemicolon/markup.


Core Rules

  1. Prefer Declarative Helpers Over Branching: Avoid writing inline JavaScript ternary operations or imperative functions inside template HTML strings. Always favor built-in Markup helpers (when, repeat, pick, is, etc.).
  2. Keep Side Effects Out of Render Functions: Keep template rendering strictly side-effect-free. Side effects should live inside effect() blocks or Web Component lifecycle hooks (such as onMount).
  3. State Immutability: Do not mutate objects used as canonical state directly. Track derived/pending changes separately and merge them only at render time.
  4. Preserve Source Collections: Keep source arrays intact when deriving filtered or sorted views. Clearing a filter or search input should restore the full source list without requiring manual data resets.
  5. Reactive Value Binding: Bind props and state directly (e.g. disabled="${isDisabled}" instead of disabled="${isDisabled()}") to let Markup set up listeners surgically.

Refactor Workflows

Here is how you can migrate traditional imperative habits into clean, declarative Markup code.

1. Conditional UI (if/else)

Avoid writing inline JavaScript conditions or logic gates inside templates.

Imperative/Avoid:

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

Declarative/Prefer:

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

You can use ternary directly if you intend to render once and or dont expect the data update:

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

2. Rendering Lists

Avoid using .map() inside templates to generate dynamic list nodes. Using .map() destroys and rebuilds nodes on every update, whereas repeat() uses surgical memoization under the hood.

Imperative/Avoid:

javascript
1html`2    <ul>3        ${() => items().map((item) => html`<li>${item.name}</li>`)}4    </ul>5`

Declarative/Prefer:

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

You can use the map directly if you intend to render once and or dont expect updates:

javascript
1html`2    <ul>3        ${list.map((item) => html`<li>${item.name}</li>`)}4    </ul>5`

3. Nested Optional Reads

Avoid using nested optional chaining (?.) directly inside UI interpolations. This can lead to runtime errors if parts of the chain become undefined or unresolved.

Imperative/Avoid:

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

Declarative/Prefer:

javascript
1html`2    <div>3        <h2>4            ${pick(user, 'profile.details.name', (name) => name || 'Guest')}5        </h2>6    </div>7`

The pick option allows you to define fallbacks or handle the value for formatting and or additional processing.

javascript
1const over18 = (age) => (age > 18 ? 'Over 18' : 'Under 18')2 3html`4    <div>5        <h2>${pick(user, 'profile.details.age', over18)}</h2>6    </div>7`

4. Boolean Expression Composition

Avoid writing custom functions that just combine multiple states with && or ||. Compose them using Markup boolean combinators.

Imperative/Avoid:

javascript
1const canPublish = () => !isSaving() && hasChanges() && hasPermission()2 3html` <button disabled="${() => !canPublish()}">Publish</button> `

Declarative/Prefer:

javascript
1const canPublish = and(isNot(isSaving), is(hasChanges), is(hasPermission))2 3html` <button disabled="${isNot(canPublish)}">Publish</button> `

Markup invites function compososition and working with stateful functions. You should look more into how to create custom helpers to understand more.


Canonical Patterns

Stateful Search & Filter Listing

This is the standard pattern for rendering collections with dynamic filtering. The source state (items) remains completely immutable.

typescript
1import { html, state, when, repeat, is, pick } from '@beforesemicolon/markup'2 3const [query, setQuery] = state('')4const [items] = state<Project[]>([])5 6// Derive filtered list reactively7const filtered = () =>8    items().filter((p) => p.name.toLowerCase().includes(query().toLowerCase()))9 10const handleInput = (event: Event) => {11    setQuery((event.target as HTMLInputElement).value)12}13 14const View = html`15    <input value="${query}" oninput="${handleInput}" />16    <ul>17        ${repeat(18            filtered,19            (item) => html`<li>${item.name}</li>`,20            () => html`<p>No results found.</p>`21        )}22    </ul>23`

This allows state to remain immutable and you to create derived states that you use for rendering that combines different states to resolve to a desired one.

Async Slots (Suspense)

Use suspense to render async UI cleanly with error and fallback(while loading) rendering handlers:

typescript
1import { html, suspense } from '@beforesemicolon/markup'2 3const resource = async () => {4    const res = await fetch('/api/data')5 6    const data = await res.json()7 8    return html`<p>Resolved: ${data.message}</p>`9}10 11const ResourceView = html`12    ${suspense(13        resource,14        html`<p>Loading resource...</p>`,15        (err) => html`<p class="error">Error: ${err.message}</p>`16    )}17`

More Common Patterns

Here are more typical recipes you can copy-paste for common UI requirements:

Membership Checks & Option Swapping

typescript
1import { html, state, when, oneOf } from '@beforesemicolon/markup'2 3const [mode, setMode] = state<'view' | 'edit' | 'preview'>('view')4 5const View = html`6    ${when(7        oneOf(mode, ['edit', 'preview']),8        html`<button onclick="${() => setMode('view')}">Done</button>`,9        html`<button onclick="${() => setMode('edit')}">Edit</button>`10    )}11`

Reactive CSS Variables & Styles

typescript
1import { html, state } from '@beforesemicolon/markup'2 3const [gap] = state(12)4 5// Reactive style bindings cleared and updated dynamically6const Box = html`7    <div style="--gap: ${() => `${gap()}px`}; margin: ${gap}px">8        Spacing Gap: ${gap}px9    </div>10`

Nested Value Rendering

typescript
1import { html, state, pick } from '@beforesemicolon/markup'2 3const [currentEntity] = state({ details: { author: { name: 'Ada Lovelace' } } })4 5// Safe nested navigation via pick6const AuthorHeader = html`7    <h1>Written by: ${pick(currentEntity, 'details.author.name')}</h1>8`

Shared State Store

typescript
1import { state } from '@beforesemicolon/markup'2 3export const [todos, setTodos] = state<Todo[]>([])4export const [loadingState, setLoadingState] = state<5    'idle' | 'loading' | 'error'6>('idle')7 8export const fetchTodos = async () => {9    setLoadingState('loading')10    try {11        const response = await fetch('/api/todos')12        const list = await response.json()13        setTodos(list)14        setLoadingState('idle')15    } catch {16        setLoadingState('error')17    }18}

Conventions & Guardrails

edit this doc