ReferenceBest Practices

Best Practices #

Use this as an authoring checklist. For a complete setup, follow getting-started lessons; for optimization choices, use the performance guide.

SSR State Completeness #

Trace each initial binding back to server state or a component attribute. The server does not execute TypeScript field initializers or computed getters.

<h1>{{product.name}}</h1>
<if condition="product.inStock">
  <p>{{product.quantity}} available</p>
</if>
{
  "product": {
    "name": "Desk lamp",
    "inStock": true,
    "quantity": 12
  }
}

Missing text paths render empty; a missing condition identifier is falsy. A whole-binding attribute is omitted for missing or null values, while "", 0, and false remain values. Do not rely on these fallbacks to catch spelling mistakes.

Use Template Expressions, Not Shadow Observables #

Do not maintain both items and hasItems when the latter only mirrors items.length. Bind the condition directly:

<if condition="items.length">
  <p>{{items.length}} results</p>
</if>

Supported condition expressions #

Use paths, negation, comparisons, and limited logical operators:

<if condition="isLoggedIn && hasPermission">...</if>
<button ?disabled="{{currentIndex == 0}}">Previous</button>

Text bindings only look up paths. Arithmetic, function calls, ternaries, grouping parentheses, mixed &&/||, and more than five logical operators are not supported. Compute more complex initial display values on the server, then update them explicitly if browser interactions change their inputs.

Boolean Attributes #

Use @attr({ mode: 'boolean' }) for True/False State #

For a custom element's boolean API:

@attr({ mode: 'boolean' }) disabled = false;

For native or custom HTML boolean attributes, bind presence with ?:

<button ?disabled="{{isSaving}}">Save</button>
<input type="checkbox" ?checked="{{isSelected}}">

The String "false" Trap #

disabled="false" still disables a button because the attribute is present. Use actual JSON booleans, not strings.

ARIA attributes are different: they often need a literal "true" or "false" value. For example, use aria-pressed="", not ?aria-pressed.

Observable Truthiness in <if> Conditions #

WebUI uses the same rules on the server and browser. Unlike JavaScript, empty arrays and objects are falsy.

ValueTruthy?
true, a nonzero number, a nonempty stringYes
"false" or "0"Yes; both are nonempty strings
false, 0, "", null, a missing pathNo
[] or {}No
A nonempty array or objectYes, even if its contents are falsy

Common patterns #

<if condition="items">...</if>
<if condition="items.length > 1">...</if>
<if condition="!isLoading">...</if>

Use .length when comparing counts, and explicit booleans for independent conditions.

State and DOM Pitfalls #

These are WebUI-specific rules, not rules against React. Immutable state updates are valid in WebUI too. Choose the update that matches the observable contract and the operation.

1. Array Rebuild for Single-Property Toggle #

@observable observes assignments to the property; it does not automatically observe arbitrary nested mutations. This update changes one item and then notifies the setter:

toggleItem(id: string): void {
  const item = this.items.find(item => item.id === id);
  if (!item) return;
  item.selected = !item.selected;
  this.items = [...this.items];
}

An immutable .map() replacement also works. Do not remove the reassignment and assume the nested write is reactive. Use stable repeat keys to preserve row identity when membership or order changes.

2. Changed-Callback Chains #

A property callback is named <property>Changed(oldValue, newValue), such as itemsChanged. It runs after mounting for initial state, then synchronously on live assignments; template updates are batched separately.

Use these callbacks for effects that genuinely follow an assignment. Avoid chains of callbacks just to keep multiple copies of derived state in sync. Compute the initial value on the server and update it with the operation that changes its inputs, as the Todo count does.

3. Shadow Observables #

If a value is display-only, keep it in server state. If two paths can render directly, do not add another observable just to join them:

<span>{{firstName}} {{lastName}}</span>

Not every derived value can be expressed in a template. A formatted total or filtered count can be legitimate state; supply its initial value and keep its later updates together.

4. Manual DOM Sync via w-ref #

Keep state in properties, not in DOM text or attributes. Bind what the UI should display:

<button
  aria-pressed="{{isActive}}"
  ?data-active="{{isActive}}"
  @click="{toggle()}"
>
  Favorite
</button>

Use a braced ref such as w-ref="{inputEl}" for focus(), showModal(), or another imperative browser API. Use @input to read user input from the event, not a ref to synchronize application state manually.

5. Manual classList Toggle #

Bind a styling attribute and let CSS respond:

button[data-active] {
  font-weight: 700;
}

Keep layout, transitions, and animations in CSS. Honor prefers-reduced-motion when adding motion.

Route-Scoped State #

Prepare the data the current page needs, not every application collection. Projection manifests can reduce browser hydration state, but do not replace server data selection or access control.

Never put secrets in render state. See Build-Time State Projection for the optional payload optimization.

Light DOM vs Shadow DOM #

Use Light DOM when shared styles and document composition are intentional. Its CSS is global in its containing tree. Use Shadow DOM for native slots, host selectors, or a style boundary.

Unwrapped components default to Shadow. A sole bare <template> selects Light; a sole open wrapper keeps a component Shadow even in a --dom light build:

<template shadowrootmode="open">
  <slot></slot>
</template>

<slot>, :host, :host-context, and ::slotted are rejected in effective Light components. A policy wrapper with w-render or w-hydrate does not itself select a DOM mode.

Summary #

Before shipping a component:

  • Verify its initial display with JavaScript disabled.
  • Provide initial data on the server; do not derive it in a constructor.
  • Add browser behavior and observables only where used.
  • Notify observable setters when changing collections.
  • Use native controls, labeled inputs, template bindings, and stable keys.
  • Choose Light or Shadow DOM deliberately, and keep browser state public.