|
1 | | -# Server-Side Rendering & Lifecycle |
| 1 | +# Lifecycle Hooks |
2 | 2 |
|
3 | | -Server-Side Rendering (SSR) with VoltX enables you to render initial HTML on the server and seamlessly hydrate it on the client without re-rendering or flash of unstyled content. |
| 3 | +Volt's runtime exposes lifecycle hooks so you can observe mounts, run cleanup logic, and coordinate plugins without re-implementing binding internals. Hooks run consistently for both SSR hydration and client-only mounts. |
4 | 4 |
|
5 | | -## When to use SSR |
| 5 | +## Lifecycle Layers |
6 | 6 |
|
7 | | -- Content-heavy pages that benefit from SEO |
8 | | -- Applications requiring fast initial render |
9 | | -- Progressive web apps with offline capabilities |
10 | | -- When you need to support users with JavaScript disabled |
| 7 | +- **Global hooks** fire for every mount/unmount operation and are ideal for analytics, logging, or cross-cutting concerns. |
| 8 | +- **Element hooks** attach to a single DOM element and let you react to that element entering or leaving the document. |
| 9 | +- **Plugin hooks** are available while authoring custom bindings and let you scope mount/unmount work to a plugin instance. |
11 | 10 |
|
12 | | -## When to use client-side rendering (CSR) |
| 11 | +## Global Hooks |
13 | 12 |
|
14 | | -- Highly interactive single-page applications |
15 | | -- Applications behind authentication (no SEO needed) |
16 | | -- Rapid prototyping and development |
17 | | -- When server-side rendering adds unnecessary complexity |
| 13 | +Register global hooks with `registerGlobalHook(name, callback)`. The available events are: |
18 | 14 |
|
19 | | -## Concepts |
| 15 | +| Event | Position | |
| 16 | +| -------------------------- | ---------------------------------------------------------------------------------------------------- | |
| 17 | +| `beforeMount(root, scope)` | Runs right before bindings initialize | |
| 18 | +| | This is the place to patch the scope or read serialized state | |
| 19 | +| `afterMount(root, scope)` | Runs after VoltX has attached bindings and lifecycle state | |
| 20 | +| `beforeUnmount(root)` | Runs before a root is torn down, giving you time to flush pending work | |
| 21 | +| `afterUnmount(root)` | Runs after cleanup finishes | |
| 22 | +| | Use this to release global resources | |
20 | 23 |
|
21 | | -### Server-Side: Rendering Initial HTML |
| 24 | +```ts |
| 25 | +import { registerGlobalHook } from "@volt/volt"; |
22 | 26 |
|
23 | | -The server generates HTML with `data-volt` attributes and embedded state. Volt only requires: |
| 27 | +const unregister = registerGlobalHook("afterMount", (root, scope) => { |
| 28 | + console.debug("[volt] mounted", root.id, scope); |
| 29 | +}); |
24 | 30 |
|
25 | | -1. HTML elements with `data-volt-*` attributes |
26 | | -2. A `<script>` tag containing serialized state as JSON |
27 | | - |
28 | | -### Client-Side: Hydration |
29 | | - |
30 | | -Instead of re-rendering the DOM, VoltX.js "hydrates" the existing server-rendered HTML by: |
31 | | - |
32 | | -1. Reading the embedded state from the `<script>` tag |
33 | | -2. Recreating reactive signals from the serialized values |
34 | | -3. Attaching event listeners and bindings to existing DOM nodes |
35 | | -4. Preserving the existing DOM structure without modifications |
36 | | - |
37 | | -## State Serialization |
38 | | - |
39 | | -### Server-Side Pattern |
40 | | - |
41 | | -Embed initial state in a `<script>` tag with a specific ID pattern: |
42 | | - |
43 | | -```html |
44 | | -<div id="app" data-volt> |
45 | | - <script type="application/json" id="volt-state-app"> |
46 | | - {"count": 0, "username": "alice"} |
47 | | - </script> |
48 | | - |
49 | | - <p data-volt-text="count">0</p> |
50 | | - <p data-volt-text="username">alice</p> |
51 | | -</div> |
| 31 | +unregister(); |
52 | 32 | ``` |
53 | 33 |
|
54 | | -- Script tag must have `type="application/json"` |
55 | | -- ID must follow pattern: `volt-state-{element-id}` |
56 | | -- Root element must have an `id` attribute |
57 | | -- State must be valid JSON |
| 34 | +### Working with the Scope Object |
58 | 35 |
|
59 | | -### Client-Side Deserialization |
| 36 | +`beforeMount` and `afterMount` receive the reactive scope for the root element so you can read signal values or stash helpers on the scope. |
| 37 | +Avoid mutating DOM inside these hooks-leave DOM updates to bindings/plugins to prevent hydration mismatches. |
60 | 38 |
|
61 | | -Use the `hydrate()` function instead of `charge()` to hydrate all `[data-volt]` roots on the page. Volt will: |
| 39 | +### Managing Global Hooks |
62 | 40 |
|
63 | | -1. Find all elements matching the root selector (default: `[data-volt]`) |
64 | | -2. Check for embedded state in `<script>` tags |
65 | | -3. Deserialize JSON to reactive signals |
66 | | -4. Mount bindings without re-rendering |
67 | | -5. Mark elements as hydrated to prevent double-hydration |
| 41 | +- Use `unregisterGlobalHook` when the callback is no longer needed. |
| 42 | +- Call `clearGlobalHooks("beforeMount")` or `clearAllGlobalHooks()` in test teardown code to avoid cross-test leakage. |
| 43 | +- Prefer one central module to register global hooks so they are easy to audit. |
68 | 44 |
|
69 | | -## Avoiding Flash of Unstyled Content (FOUC) |
| 45 | +## Element Hooks |
70 | 46 |
|
71 | | -### CSS-Based Hiding |
| 47 | +When you need per-element notifications, register element hooks: |
72 | 48 |
|
73 | | -Hide content until VoltX.js hydrates: |
| 49 | +```ts |
| 50 | +import { registerElementHook, isElementMounted } from "@volt/volt"; |
74 | 51 |
|
75 | | -```html |
76 | | -<style> |
77 | | - [data-volt]:not([data-volt-hydrated]) { |
78 | | - visibility: hidden; |
79 | | - } |
| 52 | +const panel = document.querySelector("[data-volt-panel]"); |
80 | 53 |
|
81 | | - [data-volt][data-volt-hydrated] { |
82 | | - visibility: visible; |
83 | | - } |
84 | | -</style> |
| 54 | +registerElementHook(panel!, "mount", () => { |
| 55 | + console.log("panel is live"); |
| 56 | +}); |
85 | 57 |
|
86 | | -<div id="app" data-volt> |
87 | | - <!-- Content is hidden until hydrated --> |
88 | | -</div> |
89 | | -``` |
| 58 | +registerElementHook(panel!, "unmount", () => { |
| 59 | + console.log("panel removed, dispose timers"); |
| 60 | +}); |
90 | 61 |
|
91 | | -### Strategy 2: Loading Indicator |
92 | | - |
93 | | -Show a loading state during hydration: |
94 | | - |
95 | | -```html |
96 | | -<style> |
97 | | - .loading-overlay { |
98 | | - position: fixed; |
99 | | - inset: 0; |
100 | | - background: white; |
101 | | - display: flex; |
102 | | - align-items: center; |
103 | | - justify-content: center; |
104 | | - } |
105 | | -
|
106 | | - [data-volt-hydrated] ~ .loading-overlay { |
107 | | - display: none; |
108 | | - } |
109 | | -</style> |
110 | | - |
111 | | -<div id="app" data-volt> |
112 | | - <!-- App content --> |
113 | | -</div> |
114 | | -<div class="loading-overlay">Loading...</div> |
115 | | - |
116 | | -<script> |
117 | | - document.addEventListener('DOMContentLoaded', () => { |
118 | | - Volt.hydrate(); |
119 | | - }); |
120 | | -</script> |
| 62 | +if (isElementMounted(panel!)) { |
| 63 | + // Safe to touch DOM or read bindings immediately. |
| 64 | +} |
121 | 65 | ``` |
122 | 66 |
|
123 | | -### Progressive Enhancement |
| 67 | +Element hooks automatically dispose after the element unmounts. Use `getElementBindings(element)` when debugging to see which binding directives are attached to a node. |
124 | 68 |
|
125 | | -Render fully functional HTML that works without JavaScript, then enhance with interactivity: |
| 69 | +## Plugin Lifecycle Hooks |
126 | 70 |
|
127 | | -```html |
128 | | -<!-- Form works without JavaScript --> |
129 | | -<form id="contact" method="POST" action="/submit" data-volt> |
130 | | - <script type="application/json" id="volt-state-contact"> |
131 | | - {"submitted": false} |
132 | | - </script> |
| 71 | +Custom plugins receive lifecycle helpers on the plugin context: |
133 | 72 |
|
134 | | - <input type="email" name="email" required> |
| 73 | +```ts |
| 74 | +import type { PluginContext } from "@volt/volt"; |
135 | 75 |
|
136 | | - <!-- Enhanced with VoltX.js for client-side validation --> |
137 | | - <p data-volt-if="submitted" data-volt-text="'Thank you!'"></p> |
| 76 | +export function focusPlugin(ctx: PluginContext) { |
| 77 | + const el = ctx.element as HTMLElement; |
138 | 78 |
|
139 | | - <button type="submit">Submit</button> |
140 | | -</form> |
| 79 | + ctx.lifecycle.onMount(() => el.focus()); |
| 80 | + ctx.lifecycle.onUnmount(() => el.blur()); |
| 81 | +} |
141 | 82 | ``` |
142 | 83 |
|
143 | | -Can you believe FOUC is an [actual](https://en.wikipedia.org/wiki/Flash_of_unstyled_content) acronym? |
144 | | - |
145 | | -## Guidelines/Best Practices |
146 | | - |
147 | | -### When to Use SSR vs CSR |
148 | | - |
149 | | -**Use SSR for:** |
150 | | - |
151 | | -- Any page requiring SEO |
152 | | - |
153 | | -**Use CSR for:** |
154 | | - |
155 | | -- Complex, interactive and/or real-time applications |
156 | | - |
157 | | -### State Management |
158 | | - |
159 | | -**Do:** |
160 | | - |
161 | | -- Keep server-rendered state minimal (only essential data) |
162 | | -- Use computed signals for derived values (don't serialize them) |
163 | | -- Validate and sanitize state on the server |
164 | | -- Use consistent data structures between server and client |
165 | | - |
166 | | -**Don't:** |
| 84 | +- `ctx.lifecycle.onMount` and `ctx.lifecycle.onUnmount` let you coordinate DOM state with the binding's lifetime. |
| 85 | +- Use `ctx.lifecycle.beforeBinding` and `ctx.lifecycle.afterBinding` to measure binding creation or guard against duplicate initialization. |
| 86 | +- Always combine lifecycle hooks with `ctx.addCleanup` if you create subscriptions that outlive a single mount cycle. |
167 | 87 |
|
168 | | -- Serialize functions or complex objects |
169 | | -- Include sensitive data in client-side state |
170 | | -- Serialize computed signals (they're recalculated on hydration) |
171 | | -- Embed large datasets (fetch them after hydration instead) |
| 88 | +## Best Practices |
172 | 89 |
|
173 | | -### Security |
| 90 | +- Keep hook callbacks side-effect free whenever possible; defer heavy work to asynchronous tasks. |
| 91 | +- Never mutate the DOM tree that VoltX currently manages from `beforeMount`; wait for `afterMount` or plugin hooks instead. |
| 92 | +- When adding analytics or telemetry, remember to remove hooks on navigation or single-page route changes to avoid duplicate events. |
| 93 | +- In tests, seed hooks inside the test body and tear them down with the disposer returned from `registerGlobalHook` to preserve isolation. |
174 | 94 |
|
175 | | -- Escape user-generated content in server-rendered HTML |
176 | | -- Validate state data before serialization |
177 | | -- Use Content Security Policy (CSP) headers |
178 | | -- Sanitize JSON to prevent XSS attacks |
| 95 | +For server-rendered workflows and hydration patterns, refer to [ssr](./ssr). |
0 commit comments