Skip to content

Commit 9aa61de

Browse files
author
Andrew Ritz
committed
Improve documentation: fill critical gaps identified from blog series
1 parent 00172a8 commit 9aa61de

6 files changed

Lines changed: 866 additions & 279 deletions

File tree

docs/guide/concepts/hydration.md

Lines changed: 123 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,134 +1,171 @@
11
# Hydration & Interactivity
22

3-
WebUI renders HTML at build time with zero JavaScript. To add client-side interactivity — event handlers, reactive state, dynamic updates — you use [FAST](https://fast.design/) custom elements that **hydrate** the pre-rendered HTML on the client. This is an islands architecture: most of the page is static HTML, and only interactive components ship JavaScript.
3+
## What is Hydration?
4+
5+
WebUI renders HTML at build time (or server-render time) with **zero JavaScript**. The browser displays the HTML immediately via [Declarative Shadow DOM](https://developer.chrome.com/docs/css-ui/declarative-shadow-dom) — users see content before any script loads.
6+
7+
**Hydration** is the process of attaching event listeners and reactive bindings to the already-rendered DOM. This is _not_ re-rendering: the DOM already exists. Hydration makes it interactive.
8+
9+
WebUI uses an **islands architecture**: only interactive components ship JavaScript. Static content stays static and never loads a framework.
410

511
```
612
Build time Server render Client hydration
713
───────────── ─────────────── ─────────────────
8-
Parse HTML → Render with state → FAST reconnects
14+
Parse HTML → Render with state → Framework reconnects
915
with WebUI + inject hydration event handlers &
1016
directives markers reactive bindings
1117
```
1218

13-
## Class Definition
19+
## When to Hydrate
1420

15-
Define a custom element by extending `RenderableFASTElement(FASTElement)` and registering it with `defineAsync`:
21+
The default is **zero JavaScript**. You opt in to interactivity per-component.
1622

17-
```typescript
18-
import { FASTElement, attr, observable } from '@microsoft/fast-element';
19-
import { RenderableFASTElement } from '@microsoft/fast-html';
23+
- **Don't hydrate** pages that are purely informational — about pages, static content, read-only lists. These work with no JavaScript at all.
24+
- **Hydrate** components that need: event handlers, reactive state updates, user input, or real-time data from the browser process.
2025

21-
export class MyCounter extends RenderableFASTElement(FASTElement) {
22-
@attr count = 0;
23-
@observable label!: string;
26+
If a page has ten components but only two need click handlers, only those two ship JavaScript.
2427

25-
onIncrement(): void {
26-
this.count++;
27-
}
28-
}
29-
30-
MyCounter.defineAsync({
31-
name: 'my-counter',
32-
templateOptions: 'defer-and-hydrate',
33-
});
34-
```
35-
36-
## Templating
28+
## Two Hydration Paths
3729

38-
Each component has an HTML template with `shadowrootmode="open"`. Use <code v-pre>{{expr}}</code> for bindings:
30+
WebUI supports two hydration frameworks, chosen at build time:
3931

40-
```html
41-
<template shadowrootmode="open">
42-
<p>{{label}}: {{count}}</p>
43-
<button @click="{onIncrement()}">+1</button>
44-
</template>
32+
```bash
33+
webui build src --plugin=webui # WebUI Framework
34+
webui build src --plugin=fast # FAST-HTML
4535
```
4636

47-
## Observation
37+
### WebUI Framework (`--plugin=webui`)
4838

49-
| Decorator | Purpose | Example |
50-
|-----------|---------|---------|
51-
| `@attr` | Two-way binding with HTML attribute | `@attr title = ''` |
52-
| `@observable` | Reactive property — triggers re-render on change | `@observable items!: Item[]` |
39+
The WebUI Framework provides automatic state seeding and targeted path-indexed updates. State is restored from SSR markers during the hydration walk — no manual DOM reading required.
5340

5441
```typescript
55-
export class TodoItem extends RenderableFASTElement(FASTElement) {
56-
@attr title = ''; // <todo-item title="Buy milk">
57-
@attr state = 'pending'; // <todo-item state="done">
58-
@observable editing = false; // Internal reactive state
59-
}
60-
```
42+
import { WebUIElement, attr, observable, volatile } from '@microsoft/webui-framework';
43+
44+
export class TodoApp extends WebUIElement {
45+
@attr title = '';
46+
@observable items: TodoItemData[] = [];
6147

62-
## Events
48+
@volatile get remainingCount(): number {
49+
return this.items.filter(i => i.state !== 'done').length;
50+
}
6351

64-
Bind events with `@eventname="{handler()}"`:
52+
addInput!: HTMLInputElement;
6553

66-
| Syntax | Behavior |
67-
|--------|----------|
68-
| `@click="{onClick()}"` | Call handler, no event object |
69-
| `@click="{onClick(e)}"` | Call handler with the event object |
70-
| `@keydown="{onKey(e)}"` | Works with any DOM event |
71-
| `@custom-event="{onCustom(e)}"` | Works with `CustomEvent` (access `e.detail`) |
54+
onAddKeydown(e: KeyboardEvent): void {
55+
if (e.key === 'Enter') {
56+
const text = this.addInput.value.trim();
57+
if (!text) return;
58+
this.items = [...this.items, { id: String(Date.now()), title: text, state: 'pending' }];
59+
this.addInput.value = '';
60+
}
61+
}
62+
}
7263

73-
```html
74-
<button @click="{onSave()}">Save</button>
75-
<input @keydown="{onKeydown(e)}" />
76-
<todo-item @toggle="{onToggle(e)}"></todo-item>
64+
TodoApp.define('todo-app');
7765
```
7866

79-
## References
67+
Key characteristics:
8068

81-
Use `f-ref` to store a DOM element reference on the component instance:
69+
- **Base class:** `WebUIElement` from `@microsoft/webui-framework`
70+
- **Decorators:** `@attr`, `@observable`, `@volatile`
71+
- **Refs:** `w-ref="name"`
72+
- **State seeding:** automatic during hydration walk (no manual DOM reading)
73+
- **Update model:** targeted path-indexed updates — only bindings referencing the changed property update
74+
- **Registration:** `MyComponent.define('my-component')`
8275

83-
```html
84-
<input f-ref="{nameInput}" @keydown="{onKeydown(e)}" />
85-
```
76+
### FAST-HTML (`--plugin=fast`)
77+
78+
FAST-HTML builds on the [FAST](https://fast.design/) framework. State is restored manually in the `prepare()` method by reading the pre-rendered shadow DOM.
8679

8780
```typescript
88-
export class MyForm extends RenderableFASTElement(FASTElement) {
89-
nameInput!: HTMLInputElement; // Populated by f-ref
81+
import { FASTElement, attr, observable } from '@microsoft/fast-element';
82+
import { RenderableFASTElement } from '@microsoft/fast-html';
9083

91-
onKeydown(e: KeyboardEvent): void {
92-
if (e.key === 'Enter') {
93-
console.log(this.nameInput.value);
84+
export class TodoApp extends RenderableFASTElement(FASTElement) {
85+
@attr title = '';
86+
@observable items!: TodoItemData[];
87+
88+
async prepare(): Promise<void> {
89+
const items: TodoItemData[] = [];
90+
for (const el of this.shadowRoot!.querySelectorAll('todo-item')) {
91+
items.push({
92+
id: el.getAttribute('id') || '',
93+
title: el.getAttribute('title') || '',
94+
state: el.getAttribute('state') || 'pending',
95+
});
9496
}
97+
this.items = items;
9598
}
9699
}
100+
101+
TodoApp.defineAsync({ name: 'todo-app', templateOptions: 'defer-and-hydrate' });
97102
```
98103

99-
## Initial State
104+
Key characteristics:
100105

101-
`prepare()` is called **after hydration** — the DOM is already rendered, and FAST has reconnected bindings. Use it to restore component state from the pre-rendered shadow DOM:
106+
- **Base class:** `RenderableFASTElement(FASTElement)` from `@microsoft/fast-html`
107+
- **Decorators:** `@attr`, `@observable` (from `@microsoft/fast-element`)
108+
- **Refs:** `f-ref="{name}"`
109+
- **State seeding:** manual via `prepare()` method — read state from the pre-rendered DOM
110+
- **Registration:** `MyComponent.defineAsync({ name: '...', templateOptions: 'defer-and-hydrate' })`
102111

103-
```typescript
104-
async prepare(): Promise<void> {
105-
const items: Item[] = [];
106-
for (const el of this.shadowRoot!.querySelectorAll('todo-item')) {
107-
items.push({
108-
id: el.getAttribute('id') || '',
109-
title: el.getAttribute('title') || '',
110-
state: el.getAttribute('state') || 'pending',
111-
});
112-
}
113-
this.items = items; // Setting @observable triggers reactivity
114-
}
115-
```
112+
## Comparison
116113

117-
No flash of content — HTML is already visible from SSR, and `prepare()` silently wires up the reactive state behind it.
114+
| Aspect | WebUI Framework | FAST-HTML |
115+
|--------|----------------|-----------|
116+
| Package | `@microsoft/webui-framework` | `@microsoft/fast-html` + `@microsoft/fast-element` |
117+
| Base class | `WebUIElement` | `RenderableFASTElement(FASTElement)` |
118+
| State seeding | Automatic from SSR markers | Manual in `prepare()` |
119+
| Ref binding | `w-ref="name"` | `f-ref="{name}"` |
120+
| Update model | Targeted path-indexed | Full observable chain |
121+
| Best for | SSR-first, minimal JS | Complex client interactivity |
118122

119-
## Full Example
123+
## Hydration Lifecycle
120124

121-
See the [`todo-fast`](https://github.com/microsoft/webui/tree/main/examples/app/todo-fast) example for a complete app.
125+
Both frameworks follow the same high-level lifecycle:
122126

123-
```bash
124-
webui build examples/app/todo-fast/src --out dist --plugin=fast
127+
1. **Server renders HTML** with Declarative Shadow DOM
128+
2. **Browser parses HTML** and creates shadow roots — content is visible immediately
129+
3. **JavaScript loads** and custom elements upgrade
130+
4. **Framework detects** the existing shadow root (instead of creating a new one)
131+
5. **Walks DOM once** to connect bindings to SSR markers
132+
6. **State is seeded** — automatically (WebUI Framework) or via `prepare()` (FAST-HTML)
133+
7. **Markers are removed**, component is interactive
134+
135+
No flash of content — HTML is already visible from SSR, and hydration silently wires up the reactive state behind it.
125136

126-
webui serve examples/app/todo-fast/src \
127-
--state examples/app/todo-fast/data/state.json \
128-
--plugin=fast --watch
137+
## Performance Measurement
138+
139+
WebUI emits performance marks during hydration. Use them to verify that hydration is fast:
140+
141+
```typescript
142+
window.addEventListener('webui:hydration-complete', () => {
143+
const total = performance.getEntriesByName('webui:hydrate:total', 'measure')[0];
144+
console.log(`Hydration complete in ${total?.duration.toFixed(1)}ms`);
145+
});
146+
```
147+
148+
## Template Syntax
149+
150+
Both frameworks use the same template syntax in component HTML files:
151+
152+
```html
153+
<template shadowrootmode="open">
154+
<h1>{{title}}</h1>
155+
<button @click="{onClick()}">Click me</button>
156+
<for each="item in items">
157+
<p>{{item.name}}</p>
158+
</for>
159+
<if condition="isVisible">
160+
<span>Shown</span>
161+
</if>
162+
</template>
129163
```
130164

165+
Event binding, interpolation, conditionals, and loops work identically regardless of which framework you choose. The difference is in the TypeScript component class, not the template.
166+
131167
## Learn More
132168

133-
- [FAST documentation](https://fast.design/) — Full framework reference
134-
- [Plugins](/guide/concepts/plugins/) — How the `fast` build plugin works internally
169+
- [WebUI Framework examples](https://github.com/microsoft/webui/tree/main/examples/app/todo-webui)
170+
- [FAST-HTML examples](https://github.com/microsoft/webui/tree/main/examples/app/todo-fast)
171+
- [Plugins](/guide/concepts/plugins/) — How parser and handler plugins work

0 commit comments

Comments
 (0)