a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: split lifecycle & ssr into separate docs (#5)

authored by

Owais and committed by
GitHub
b1cf4294 c5c229ce

+244 -151
+1 -2
docs/.vitepress/config.ts
··· 45 45 { text: "Animations & Transitions", link: "/animations" }, 46 46 ], 47 47 }, 48 - { text: "Usage", collapsed: false, items: u.scanDir("usage", "/usage") }, 49 48 { 50 49 text: "CSS", 51 50 collapsed: false, 52 - items: [{ text: "Volt CSS", link: "/css/volt-css" }, { text: "Reference", link: "/css/semantics" }], 51 + items: [{ text: "VoltX CSS", link: "/css/volt-css" }, { text: "Reference", link: "/css/semantics" }], 53 52 docFooterText: "Auto-generated CSS Docs", 54 53 }, 55 54 { text: "Specs", collapsed: true, items: u.scanDir("spec", "/spec") },
+1 -1
docs/installation.md
··· 139 139 </script> 140 140 ``` 141 141 142 - See the [Server-Side Rendering & Lifecycle](./usage/lifecycle) documentation for complete SSR patterns and hydration strategies. 142 + See the [Server-Side Rendering guide](./usage/ssr) for complete hydration patterns and lifecycle coordination tips. 143 143 144 144 ## Plugin Setup 145 145
+2 -1
docs/usage/counter.md
··· 285 285 - [State Management](./state) for advanced signal patterns 286 286 - [Bindings](./bindings) for complete binding reference 287 287 - [Expressions](./expressions) for template syntax details 288 - - [Lifecycle](./lifecycle) for SSR and hydration 288 + - [Lifecycle](./lifecycle) for mount/unmount hooks 289 + - [Server-Side Rendering](./ssr) for hydration strategies
+62 -145
docs/usage/lifecycle.md
··· 1 - # Server-Side Rendering & Lifecycle 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. 4 - 5 - ## When to use SSR 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 11 - 12 - ## When to use client-side rendering (CSR) 13 - 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 18 - 19 - ## Concepts 1 + # Lifecycle Hooks 20 2 21 - ### Server-Side: Rendering Initial HTML 22 - 23 - The server generates HTML with `data-volt` attributes and embedded state. Volt only requires: 24 - 25 - 1. HTML elements with `data-volt-*` attributes 26 - 2. A `<script>` tag containing serialized state as JSON 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. 27 4 28 - ### Client-Side: Hydration 5 + ## Lifecycle Layers 29 6 30 - Instead of re-rendering the DOM, VoltX.js "hydrates" the existing server-rendered HTML by: 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. 31 10 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 11 + ## Global Hooks 36 12 37 - ## State Serialization 13 + Register global hooks with `registerGlobalHook(name, callback)`. The available events are: 38 14 39 - ### Server-Side Pattern 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 | 40 23 41 - Embed initial state in a `<script>` tag with a specific ID pattern: 24 + ```ts 25 + import { registerGlobalHook } from "@volt/volt"; 42 26 43 - ```html 44 - <div id="app" data-volt> 45 - <script type="application/json" id="volt-state-app"> 46 - {"count": 0, "username": "alice"} 47 - </script> 27 + const unregister = registerGlobalHook("afterMount", (root, scope) => { 28 + console.debug("[volt] mounted", root.id, scope); 29 + }); 48 30 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 - ``` 90 - 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> 58 + registerElementHook(panel!, "unmount", () => { 59 + console.log("panel removed, dispose timers"); 60 + }); 115 61 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 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. 148 87 149 - **Use SSR for:** 88 + ## Best Practices 150 89 151 - - Any page requiring SEO 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. 152 94 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:** 167 - 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) 172 - 173 - ### Security 174 - 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).
+176
docs/usage/ssr.md
··· 1 + # Server-Side Rendering 2 + 3 + VoltX can render HTML on the server and hydrate it on the client so that the initial paint is fast and SEO-friendly without sacrificing interactivity. 4 + 5 + ## When SSR Helps 6 + 7 + - Marketing and content-heavy pages that rely on search indexing. 8 + - Dashboards that must show current data immediately on first paint. 9 + - Progressive enhancement flows where the page should work without JavaScript. 10 + - Latency-sensitive experiences served to slow devices or connections. 11 + 12 + ## When CSR Is Enough 13 + 14 + - Highly interactive applications dominated by client-side state. 15 + - Authenticated surfaces hidden from crawlers. 16 + - Rapid prototypes where deployment speed outweighs initial paint. 17 + - Workloads where duplicating rendering logic on the server adds complexity without user benefit. 18 + 19 + ## Rendering Flow 20 + 21 + 1. Render HTML on the server and embed serialized state. 22 + 2. Ship that HTML to the browser. 23 + 3. Call `hydrate()` to attach VoltX bindings without re-rendering. 24 + 25 + ### Produce Markup on the Server 26 + 27 + Use `serializeScope()` to convert reactive state into JSON before embedding it in the HTML you return: 28 + 29 + ```ts 30 + import { serializeScope, signal } from "@volt/volt"; 31 + 32 + export function renderCounter() { 33 + const scope = { 34 + count: signal(0), 35 + label: "Visitors", 36 + }; 37 + 38 + const serialized = serializeScope(scope); 39 + 40 + return ` 41 + <div id="counter" data-volt> 42 + <script type="application/json" id="volt-state-counter"> 43 + ${serialized} 44 + </script> 45 + 46 + <h2 data-volt-text="label">${scope.label}</h2> 47 + <button data-volt-on:click="count++">Clicked <span data-volt-text="count">${scope.count.get()}</span> times</button> 48 + </div> 49 + `; 50 + } 51 + ``` 52 + 53 + Guidelines: 54 + 55 + - Every server-rendered root must have an `id`. VoltX looks for `<script id="volt-state-{id}">` next to it. 56 + - The script tag must use `type="application/json"` and contain valid JSON. Pretty printing is fine; whitespace is ignored. 57 + - Keep serialized data minimal. Fetch large collections after hydration. 58 + 59 + ### Send HTML to the Client 60 + 61 + The HTML you return should already contain the serialized state script tag. VoltX will reuse the DOM structure; do **not** re-render the same tree on the client. 62 + 63 + ### Hydrate in the Browser 64 + 65 + Call `hydrate()` once the page loads. It discovers `[data-volt]` roots automatically. 66 + 67 + ```ts 68 + import { hydrate } from "@volt/volt"; 69 + 70 + document.addEventListener("DOMContentLoaded", () => { 71 + hydrate({ 72 + rootSelector: "[data-volt]", 73 + skipHydrated: true, // defaults to true; repeat calls ignore already hydrated roots 74 + }); 75 + }); 76 + ``` 77 + 78 + If you only hydrate a specific block, pass a narrower selector or manually select elements and call `hydrate({ rootSelector: "#counter" })`. 79 + 80 + ## Serialized State 81 + 82 + VoltX exposes helpers that mirror the runtime's internal behavior: 83 + 84 + | Helper | Action | 85 + | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------- | 86 + | `serializeScope(scope)` | Converts signals into their raw values before you embed them | 87 + | `deserializeScope(data)` | Restores a JSON payload into a fresh scope. Useful for streaming responses or server actions that return HTML partials | 88 + | `isServerRendered(element)` | Tells you if VoltX found a matching serialized state block | 89 + | `isHydrated(element)` | Detects whether a root has already been hydrated, which is handy when mixing SSR content with dynamic client mounts | 90 + | `getSerializedState(element)` | Reads the JSON payload for debugging or custom hydration flows. | 91 + 92 + ```ts 93 + import { deserializeScope, getSerializedState } from "@volt/volt"; 94 + 95 + const root = document.querySelector("#counter")!; 96 + const state = getSerializedState(root); 97 + 98 + if (state) { 99 + const scope = deserializeScope(state); 100 + console.log(scope.count.get()); // -> 0 101 + } 102 + ``` 103 + 104 + ## Avoiding Flash of Unstyled Content 105 + 106 + Hydrated markup should look identical before and after VoltX runs. When CSS or font loading causes flicker, consider these patterns: 107 + 108 + ### Hide Until Hydrated 109 + 110 + ```html 111 + <style> 112 + [data-volt]:not([data-volt-hydrated]) { 113 + visibility: hidden; 114 + } 115 + 116 + [data-volt][data-volt-hydrated] { 117 + visibility: visible; 118 + } 119 + </style> 120 + ``` 121 + 122 + VoltX sets `data-volt-hydrated="true"` once `hydrate()` completes, so you can safely reveal content at that point. 123 + 124 + ### Use a Loading Overlay 125 + 126 + ```html 127 + <div id="app" data-volt> 128 + <!-- server-rendered content --> 129 + </div> 130 + <div class="loading-overlay">Loading…</div> 131 + 132 + <script type="module"> 133 + import { hydrate } from "@volt/volt"; 134 + 135 + hydrate(); 136 + </script> 137 + ``` 138 + 139 + ```css 140 + .loading-overlay { 141 + position: fixed; 142 + inset: 0; 143 + background: white; 144 + display: grid; 145 + place-items: center; 146 + } 147 + 148 + [data-volt-hydrated] ~ .loading-overlay { 149 + display: none; 150 + } 151 + ``` 152 + 153 + ### Progressive Enhancement 154 + 155 + Render functional HTML that works without JavaScript, then let VoltX enhance it: 156 + 157 + ```html 158 + <form id="contact" method="POST" action="/submit" data-volt> 159 + <script type="application/json" id="volt-state-contact"> 160 + { "submitted": false } 161 + </script> 162 + 163 + <input type="email" name="email" required /> 164 + <p data-volt-if="submitted" data-volt-text="'Thank you!'"></p> 165 + <button type="submit">Submit</button> 166 + </form> 167 + ``` 168 + 169 + ## Security Checklist 170 + 171 + - Escape user-generated content in the HTML you render. 172 + - Validate JSON before embedding it in `<script type="application/json">` tags. 173 + - Apply a strict Content Security Policy (CSP) so inline scripts are controlled. 174 + - Never serialize secrets. Treat the hydrated payload as public data. 175 + 176 + Pair these guidelines with the lifecycle hooks documented in [lifecycle](./lifecycle) to coordinate mount-time work across SSR and client renders.
+2 -2
docs/usage/state.md
··· 37 37 - Logging or analytics 38 38 - Coordinating multiple signals 39 39 40 - For asynchronous operations, use `asyncEffect()` (see [asyncEffect](./async-effect.md)) which handles cleanup of pending operations when dependencies change or the effect is disposed. 40 + For asynchronous operations, use `asyncEffect()` (see [asyncEffect](./async-effect)) which handles cleanup of pending operations when dependencies change or the effect is disposed. 41 41 42 42 ## Declarative State 43 43 ··· 109 109 110 110 Only serialize base signals containing primitive values, arrays, and plain objects. Computed signals are recalculated during hydration and should not be serialized. 111 111 112 - See the [Server-Side Rendering & Lifecycle](./lifecycle.md) documentation for complete SSR patterns. 112 + See the [Server-Side Rendering guide](./ssr) for complete hydration patterns. 113 113 114 114 ## Guidelines 115 115