|
1 | 1 | # Hydration & Interactivity |
2 | 2 |
|
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. |
4 | 10 |
|
5 | 11 | ``` |
6 | 12 | Build time Server render Client hydration |
7 | 13 | ───────────── ─────────────── ───────────────── |
8 | | -Parse HTML → Render with state → FAST reconnects |
| 14 | +Parse HTML → Render with state → Framework reconnects |
9 | 15 | with WebUI + inject hydration event handlers & |
10 | 16 | directives markers reactive bindings |
11 | 17 | ``` |
12 | 18 |
|
13 | | -## Class Definition |
| 19 | +## When to Hydrate |
14 | 20 |
|
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. |
16 | 22 |
|
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. |
20 | 25 |
|
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. |
24 | 27 |
|
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 |
37 | 29 |
|
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: |
39 | 31 |
|
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 |
45 | 35 | ``` |
46 | 36 |
|
47 | | -## Observation |
| 37 | +### WebUI Framework (`--plugin=webui`) |
48 | 38 |
|
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. |
53 | 40 |
|
54 | 41 | ```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[] = []; |
61 | 47 |
|
62 | | -## Events |
| 48 | + @volatile get remainingCount(): number { |
| 49 | + return this.items.filter(i => i.state !== 'done').length; |
| 50 | + } |
63 | 51 |
|
64 | | -Bind events with `@eventname="{handler()}"`: |
| 52 | + addInput!: HTMLInputElement; |
65 | 53 |
|
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 | +} |
72 | 63 |
|
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'); |
77 | 65 | ``` |
78 | 66 |
|
79 | | -## References |
| 67 | +Key characteristics: |
80 | 68 |
|
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')` |
82 | 75 |
|
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. |
86 | 79 |
|
87 | 80 | ```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'; |
90 | 83 |
|
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 | + }); |
94 | 96 | } |
| 97 | + this.items = items; |
95 | 98 | } |
96 | 99 | } |
| 100 | + |
| 101 | +TodoApp.defineAsync({ name: 'todo-app', templateOptions: 'defer-and-hydrate' }); |
97 | 102 | ``` |
98 | 103 |
|
99 | | -## Initial State |
| 104 | +Key characteristics: |
100 | 105 |
|
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' })` |
102 | 111 |
|
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 |
116 | 113 |
|
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 | |
118 | 122 |
|
119 | | -## Full Example |
| 123 | +## Hydration Lifecycle |
120 | 124 |
|
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: |
122 | 126 |
|
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. |
125 | 136 |
|
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> |
129 | 163 | ``` |
130 | 164 |
|
| 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 | + |
131 | 167 | ## Learn More |
132 | 168 |
|
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