···139139</script>
140140```
141141142142-See the [Server-Side Rendering & Lifecycle](./usage/lifecycle) documentation for complete SSR patterns and hydration strategies.
142142+See the [Server-Side Rendering guide](./usage/ssr) for complete hydration patterns and lifecycle coordination tips.
143143144144## Plugin Setup
145145
+2-1
docs/usage/counter.md
···285285- [State Management](./state) for advanced signal patterns
286286- [Bindings](./bindings) for complete binding reference
287287- [Expressions](./expressions) for template syntax details
288288-- [Lifecycle](./lifecycle) for SSR and hydration
288288+- [Lifecycle](./lifecycle) for mount/unmount hooks
289289+- [Server-Side Rendering](./ssr) for hydration strategies
+62-145
docs/usage/lifecycle.md
···11-# Server-Side Rendering & Lifecycle
22-33-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.
44-55-## When to use SSR
66-77-- Content-heavy pages that benefit from SEO
88-- Applications requiring fast initial render
99-- Progressive web apps with offline capabilities
1010-- When you need to support users with JavaScript disabled
1111-1212-## When to use client-side rendering (CSR)
1313-1414-- Highly interactive single-page applications
1515-- Applications behind authentication (no SEO needed)
1616-- Rapid prototyping and development
1717-- When server-side rendering adds unnecessary complexity
1818-1919-## Concepts
11+# Lifecycle Hooks
2022121-### Server-Side: Rendering Initial HTML
2222-2323-The server generates HTML with `data-volt` attributes and embedded state. Volt only requires:
2424-2525-1. HTML elements with `data-volt-*` attributes
2626-2. A `<script>` tag containing serialized state as JSON
33+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.
2742828-### Client-Side: Hydration
55+## Lifecycle Layers
2963030-Instead of re-rendering the DOM, VoltX.js "hydrates" the existing server-rendered HTML by:
77+- **Global hooks** fire for every mount/unmount operation and are ideal for analytics, logging, or cross-cutting concerns.
88+- **Element hooks** attach to a single DOM element and let you react to that element entering or leaving the document.
99+- **Plugin hooks** are available while authoring custom bindings and let you scope mount/unmount work to a plugin instance.
31103232-1. Reading the embedded state from the `<script>` tag
3333-2. Recreating reactive signals from the serialized values
3434-3. Attaching event listeners and bindings to existing DOM nodes
3535-4. Preserving the existing DOM structure without modifications
1111+## Global Hooks
36123737-## State Serialization
1313+Register global hooks with `registerGlobalHook(name, callback)`. The available events are:
38143939-### Server-Side Pattern
1515+| Event | Position |
1616+| -------------------------- | ---------------------------------------------------------------------------------------------------- |
1717+| `beforeMount(root, scope)` | Runs right before bindings initialize |
1818+| | This is the place to patch the scope or read serialized state |
1919+| `afterMount(root, scope)` | Runs after VoltX has attached bindings and lifecycle state |
2020+| `beforeUnmount(root)` | Runs before a root is torn down, giving you time to flush pending work |
2121+| `afterUnmount(root)` | Runs after cleanup finishes |
2222+| | Use this to release global resources |
40234141-Embed initial state in a `<script>` tag with a specific ID pattern:
2424+```ts
2525+import { registerGlobalHook } from "@volt/volt";
42264343-```html
4444-<div id="app" data-volt>
4545- <script type="application/json" id="volt-state-app">
4646- {"count": 0, "username": "alice"}
4747- </script>
2727+const unregister = registerGlobalHook("afterMount", (root, scope) => {
2828+ console.debug("[volt] mounted", root.id, scope);
2929+});
48304949- <p data-volt-text="count">0</p>
5050- <p data-volt-text="username">alice</p>
5151-</div>
3131+unregister();
5232```
53335454-- Script tag must have `type="application/json"`
5555-- ID must follow pattern: `volt-state-{element-id}`
5656-- Root element must have an `id` attribute
5757-- State must be valid JSON
3434+### Working with the Scope Object
58355959-### Client-Side Deserialization
3636+`beforeMount` and `afterMount` receive the reactive scope for the root element so you can read signal values or stash helpers on the scope.
3737+Avoid mutating DOM inside these hooks-leave DOM updates to bindings/plugins to prevent hydration mismatches.
60386161-Use the `hydrate()` function instead of `charge()` to hydrate all `[data-volt]` roots on the page. Volt will:
3939+### Managing Global Hooks
62406363-1. Find all elements matching the root selector (default: `[data-volt]`)
6464-2. Check for embedded state in `<script>` tags
6565-3. Deserialize JSON to reactive signals
6666-4. Mount bindings without re-rendering
6767-5. Mark elements as hydrated to prevent double-hydration
4141+- Use `unregisterGlobalHook` when the callback is no longer needed.
4242+- Call `clearGlobalHooks("beforeMount")` or `clearAllGlobalHooks()` in test teardown code to avoid cross-test leakage.
4343+- Prefer one central module to register global hooks so they are easy to audit.
68446969-## Avoiding Flash of Unstyled Content (FOUC)
4545+## Element Hooks
70467171-### CSS-Based Hiding
4747+When you need per-element notifications, register element hooks:
72487373-Hide content until VoltX.js hydrates:
4949+```ts
5050+import { registerElementHook, isElementMounted } from "@volt/volt";
74517575-```html
7676-<style>
7777- [data-volt]:not([data-volt-hydrated]) {
7878- visibility: hidden;
7979- }
5252+const panel = document.querySelector("[data-volt-panel]");
80538181- [data-volt][data-volt-hydrated] {
8282- visibility: visible;
8383- }
8484-</style>
5454+registerElementHook(panel!, "mount", () => {
5555+ console.log("panel is live");
5656+});
85578686-<div id="app" data-volt>
8787- <!-- Content is hidden until hydrated -->
8888-</div>
8989-```
9090-9191-### Strategy 2: Loading Indicator
9292-9393-Show a loading state during hydration:
9494-9595-```html
9696-<style>
9797- .loading-overlay {
9898- position: fixed;
9999- inset: 0;
100100- background: white;
101101- display: flex;
102102- align-items: center;
103103- justify-content: center;
104104- }
105105-106106- [data-volt-hydrated] ~ .loading-overlay {
107107- display: none;
108108- }
109109-</style>
110110-111111-<div id="app" data-volt>
112112- <!-- App content -->
113113-</div>
114114-<div class="loading-overlay">Loading...</div>
5858+registerElementHook(panel!, "unmount", () => {
5959+ console.log("panel removed, dispose timers");
6060+});
11561116116-<script>
117117- document.addEventListener('DOMContentLoaded', () => {
118118- Volt.hydrate();
119119- });
120120-</script>
6262+if (isElementMounted(panel!)) {
6363+ // Safe to touch DOM or read bindings immediately.
6464+}
12165```
12266123123-### Progressive Enhancement
6767+Element hooks automatically dispose after the element unmounts. Use `getElementBindings(element)` when debugging to see which binding directives are attached to a node.
12468125125-Render fully functional HTML that works without JavaScript, then enhance with interactivity:
6969+## Plugin Lifecycle Hooks
12670127127-```html
128128-<!-- Form works without JavaScript -->
129129-<form id="contact" method="POST" action="/submit" data-volt>
130130- <script type="application/json" id="volt-state-contact">
131131- {"submitted": false}
132132- </script>
7171+Custom plugins receive lifecycle helpers on the plugin context:
13372134134- <input type="email" name="email" required>
7373+```ts
7474+import type { PluginContext } from "@volt/volt";
13575136136- <!-- Enhanced with VoltX.js for client-side validation -->
137137- <p data-volt-if="submitted" data-volt-text="'Thank you!'"></p>
7676+export function focusPlugin(ctx: PluginContext) {
7777+ const el = ctx.element as HTMLElement;
13878139139- <button type="submit">Submit</button>
140140-</form>
7979+ ctx.lifecycle.onMount(() => el.focus());
8080+ ctx.lifecycle.onUnmount(() => el.blur());
8181+}
14182```
14283143143-Can you believe FOUC is an [actual](https://en.wikipedia.org/wiki/Flash_of_unstyled_content) acronym?
144144-145145-## Guidelines/Best Practices
146146-147147-### When to Use SSR vs CSR
8484+- `ctx.lifecycle.onMount` and `ctx.lifecycle.onUnmount` let you coordinate DOM state with the binding's lifetime.
8585+- Use `ctx.lifecycle.beforeBinding` and `ctx.lifecycle.afterBinding` to measure binding creation or guard against duplicate initialization.
8686+- Always combine lifecycle hooks with `ctx.addCleanup` if you create subscriptions that outlive a single mount cycle.
14887149149-**Use SSR for:**
8888+## Best Practices
15089151151-- Any page requiring SEO
9090+- Keep hook callbacks side-effect free whenever possible; defer heavy work to asynchronous tasks.
9191+- Never mutate the DOM tree that VoltX currently manages from `beforeMount`; wait for `afterMount` or plugin hooks instead.
9292+- When adding analytics or telemetry, remember to remove hooks on navigation or single-page route changes to avoid duplicate events.
9393+- In tests, seed hooks inside the test body and tear them down with the disposer returned from `registerGlobalHook` to preserve isolation.
15294153153-**Use CSR for:**
154154-155155-- Complex, interactive and/or real-time applications
156156-157157-### State Management
158158-159159-**Do:**
160160-161161-- Keep server-rendered state minimal (only essential data)
162162-- Use computed signals for derived values (don't serialize them)
163163-- Validate and sanitize state on the server
164164-- Use consistent data structures between server and client
165165-166166-**Don't:**
167167-168168-- Serialize functions or complex objects
169169-- Include sensitive data in client-side state
170170-- Serialize computed signals (they're recalculated on hydration)
171171-- Embed large datasets (fetch them after hydration instead)
172172-173173-### Security
174174-175175-- Escape user-generated content in server-rendered HTML
176176-- Validate state data before serialization
177177-- Use Content Security Policy (CSP) headers
178178-- Sanitize JSON to prevent XSS attacks
9595+For server-rendered workflows and hydration patterns, refer to [ssr](./ssr).
+176
docs/usage/ssr.md
···11+# Server-Side Rendering
22+33+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.
44+55+## When SSR Helps
66+77+- Marketing and content-heavy pages that rely on search indexing.
88+- Dashboards that must show current data immediately on first paint.
99+- Progressive enhancement flows where the page should work without JavaScript.
1010+- Latency-sensitive experiences served to slow devices or connections.
1111+1212+## When CSR Is Enough
1313+1414+- Highly interactive applications dominated by client-side state.
1515+- Authenticated surfaces hidden from crawlers.
1616+- Rapid prototypes where deployment speed outweighs initial paint.
1717+- Workloads where duplicating rendering logic on the server adds complexity without user benefit.
1818+1919+## Rendering Flow
2020+2121+1. Render HTML on the server and embed serialized state.
2222+2. Ship that HTML to the browser.
2323+3. Call `hydrate()` to attach VoltX bindings without re-rendering.
2424+2525+### Produce Markup on the Server
2626+2727+Use `serializeScope()` to convert reactive state into JSON before embedding it in the HTML you return:
2828+2929+```ts
3030+import { serializeScope, signal } from "@volt/volt";
3131+3232+export function renderCounter() {
3333+ const scope = {
3434+ count: signal(0),
3535+ label: "Visitors",
3636+ };
3737+3838+ const serialized = serializeScope(scope);
3939+4040+ return `
4141+ <div id="counter" data-volt>
4242+ <script type="application/json" id="volt-state-counter">
4343+ ${serialized}
4444+ </script>
4545+4646+ <h2 data-volt-text="label">${scope.label}</h2>
4747+ <button data-volt-on:click="count++">Clicked <span data-volt-text="count">${scope.count.get()}</span> times</button>
4848+ </div>
4949+ `;
5050+}
5151+```
5252+5353+Guidelines:
5454+5555+- Every server-rendered root must have an `id`. VoltX looks for `<script id="volt-state-{id}">` next to it.
5656+- The script tag must use `type="application/json"` and contain valid JSON. Pretty printing is fine; whitespace is ignored.
5757+- Keep serialized data minimal. Fetch large collections after hydration.
5858+5959+### Send HTML to the Client
6060+6161+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.
6262+6363+### Hydrate in the Browser
6464+6565+Call `hydrate()` once the page loads. It discovers `[data-volt]` roots automatically.
6666+6767+```ts
6868+import { hydrate } from "@volt/volt";
6969+7070+document.addEventListener("DOMContentLoaded", () => {
7171+ hydrate({
7272+ rootSelector: "[data-volt]",
7373+ skipHydrated: true, // defaults to true; repeat calls ignore already hydrated roots
7474+ });
7575+});
7676+```
7777+7878+If you only hydrate a specific block, pass a narrower selector or manually select elements and call `hydrate({ rootSelector: "#counter" })`.
7979+8080+## Serialized State
8181+8282+VoltX exposes helpers that mirror the runtime's internal behavior:
8383+8484+| Helper | Action |
8585+| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
8686+| `serializeScope(scope)` | Converts signals into their raw values before you embed them |
8787+| `deserializeScope(data)` | Restores a JSON payload into a fresh scope. Useful for streaming responses or server actions that return HTML partials |
8888+| `isServerRendered(element)` | Tells you if VoltX found a matching serialized state block |
8989+| `isHydrated(element)` | Detects whether a root has already been hydrated, which is handy when mixing SSR content with dynamic client mounts |
9090+| `getSerializedState(element)` | Reads the JSON payload for debugging or custom hydration flows. |
9191+9292+```ts
9393+import { deserializeScope, getSerializedState } from "@volt/volt";
9494+9595+const root = document.querySelector("#counter")!;
9696+const state = getSerializedState(root);
9797+9898+if (state) {
9999+ const scope = deserializeScope(state);
100100+ console.log(scope.count.get()); // -> 0
101101+}
102102+```
103103+104104+## Avoiding Flash of Unstyled Content
105105+106106+Hydrated markup should look identical before and after VoltX runs. When CSS or font loading causes flicker, consider these patterns:
107107+108108+### Hide Until Hydrated
109109+110110+```html
111111+<style>
112112+ [data-volt]:not([data-volt-hydrated]) {
113113+ visibility: hidden;
114114+ }
115115+116116+ [data-volt][data-volt-hydrated] {
117117+ visibility: visible;
118118+ }
119119+</style>
120120+```
121121+122122+VoltX sets `data-volt-hydrated="true"` once `hydrate()` completes, so you can safely reveal content at that point.
123123+124124+### Use a Loading Overlay
125125+126126+```html
127127+<div id="app" data-volt>
128128+ <!-- server-rendered content -->
129129+</div>
130130+<div class="loading-overlay">Loading…</div>
131131+132132+<script type="module">
133133+ import { hydrate } from "@volt/volt";
134134+135135+ hydrate();
136136+</script>
137137+```
138138+139139+```css
140140+.loading-overlay {
141141+ position: fixed;
142142+ inset: 0;
143143+ background: white;
144144+ display: grid;
145145+ place-items: center;
146146+}
147147+148148+[data-volt-hydrated] ~ .loading-overlay {
149149+ display: none;
150150+}
151151+```
152152+153153+### Progressive Enhancement
154154+155155+Render functional HTML that works without JavaScript, then let VoltX enhance it:
156156+157157+```html
158158+<form id="contact" method="POST" action="/submit" data-volt>
159159+ <script type="application/json" id="volt-state-contact">
160160+ { "submitted": false }
161161+ </script>
162162+163163+ <input type="email" name="email" required />
164164+ <p data-volt-if="submitted" data-volt-text="'Thank you!'"></p>
165165+ <button type="submit">Submit</button>
166166+</form>
167167+```
168168+169169+## Security Checklist
170170+171171+- Escape user-generated content in the HTML you render.
172172+- Validate JSON before embedding it in `<script type="application/json">` tags.
173173+- Apply a strict Content Security Policy (CSP) so inline scripts are controlled.
174174+- Never serialize secrets. Treat the hydrated payload as public data.
175175+176176+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
···3737- Logging or analytics
3838- Coordinating multiple signals
39394040-For asynchronous operations, use `asyncEffect()` (see [asyncEffect](./async-effect.md)) which handles cleanup of pending operations when dependencies change or the effect is disposed.
4040+For asynchronous operations, use `asyncEffect()` (see [asyncEffect](./async-effect)) which handles cleanup of pending operations when dependencies change or the effect is disposed.
41414242## Declarative State
4343···109109110110Only serialize base signals containing primitive values, arrays, and plain objects. Computed signals are recalculated during hydration and should not be serialized.
111111112112-See the [Server-Side Rendering & Lifecycle](./lifecycle.md) documentation for complete SSR patterns.
112112+See the [Server-Side Rendering guide](./ssr) for complete hydration patterns.
113113114114## Guidelines
115115