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
- 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.). - 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 asonMount). - State Immutability: Do not mutate objects used as canonical state directly. Track derived/pending changes separately and merge them only at render time.
- 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.
- Reactive Value Binding: Bind props and state directly (e.g.
disabled="${isDisabled}"instead ofdisabled="${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:
1html`2 <div>3 ${() => (isLoading() ? html`<p>Loading...</p>` : html`<p>Loaded!</p>`)}4 </div>5`Declarative/Prefer:
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:
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:
1html`2 <ul>3 ${() => items().map((item) => html`<li>${item.name}</li>`)}4 </ul>5`Declarative/Prefer:
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:
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:
1html`2 <div>3 <h2>${() => user()?.profile?.details?.name || 'Guest'}</h2>4 </div>5`Declarative/Prefer:
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.
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:
1const canPublish = () => !isSaving() && hasChanges() && hasPermission()2 3html` <button disabled="${() => !canPublish()}">Publish</button> `Declarative/Prefer:
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.
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:
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
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
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
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
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
- Pass Getters/Functions directly: Do not execute getters inside template attributes when subscription/reactivity is intended. Pass
disabled="${isDisabled}", notdisabled="${isDisabled()}". - Clean Event Bindings: Do not wrap callbacks in redundant closures unless passing arguments:
- Good:
<button onclick="${logout}">Logout</button> - Good:
<button onclick="${() => handleSelect(item)}">Select</button> - Avoid:
<button onclick="${() => logout()}">Logout</button>
- Good:
- Direct Property Bindings: Do not pre-normalize simple template attributes in setup/getters just to render them. Markup handles
undefined, empty, and falsy attribute values correctly. - Boolean Attributes: Markup core automatically unwraps and evaluates boolean states. Do not add
Boolean(val(this.props.someBoolean()))-style wrappers; bind properties directly. - Static vs. Reactive: If a value is static (never changes after initialization), render its evaluated state:
html${state()}
. If it is reactive and should dynamically update, bind the getter:html<p>${state}</p>.