Lifecycle Hooks
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.
Lifecycle Layers
- Global hooks fire for every mount/unmount operation and are ideal for analytics, logging, or cross-cutting concerns.
- Element hooks attach to a single DOM element and let you react to that element entering or leaving the document.
- Plugin hooks are available while authoring custom bindings and let you scope mount/unmount work to a plugin instance.
Global Hooks
Register global hooks with registerGlobalHook(name, callback). The available events are:
| Event | Position |
|---|---|
beforeMount(root, scope) | Runs right before bindings initialize |
| This is the place to patch the scope or read serialized state | |
afterMount(root, scope) | Runs after VoltX has attached bindings and lifecycle state |
beforeUnmount(root) | Runs before a root is torn down, giving you time to flush pending work |
afterUnmount(root) | Runs after cleanup finishes |
| Use this to release global resources |
import { registerGlobalHook } from "@volt/volt";
const unregister = registerGlobalHook("afterMount", (root, scope) => {
console.debug("[volt] mounted", root.id, scope);
});
unregister();Working with the Scope Object
beforeMount and afterMount receive the reactive scope for the root element so you can read signal values or stash helpers on the scope. Avoid mutating DOM inside these hooks-leave DOM updates to bindings/plugins to prevent hydration mismatches.
Managing Global Hooks
- Use
unregisterGlobalHookwhen the callback is no longer needed. - Call
clearGlobalHooks("beforeMount")orclearAllGlobalHooks()in test teardown code to avoid cross-test leakage. - Prefer one central module to register global hooks so they are easy to audit.
Element Hooks
When you need per-element notifications, register element hooks:
import { registerElementHook, isElementMounted } from "@volt/volt";
const panel = document.querySelector("[data-volt-panel]");
registerElementHook(panel!, "mount", () => {
console.log("panel is live");
});
registerElementHook(panel!, "unmount", () => {
console.log("panel removed, dispose timers");
});
if (isElementMounted(panel!)) {
// Safe to touch DOM or read bindings immediately.
}Element hooks automatically dispose after the element unmounts. Use getElementBindings(element) when debugging to see which binding directives are attached to a node.
Plugin Lifecycle Hooks
Custom plugins receive lifecycle helpers on the plugin context:
import type { PluginContext } from "@volt/volt";
export function focusPlugin(ctx: PluginContext) {
const el = ctx.element as HTMLElement;
ctx.lifecycle.onMount(() => el.focus());
ctx.lifecycle.onUnmount(() => el.blur());
}ctx.lifecycle.onMountandctx.lifecycle.onUnmountlet you coordinate DOM state with the binding's lifetime.- Use
ctx.lifecycle.beforeBindingandctx.lifecycle.afterBindingto measure binding creation or guard against duplicate initialization. - Always combine lifecycle hooks with
ctx.addCleanupif you create subscriptions that outlive a single mount cycle.
Best Practices
- Keep hook callbacks side-effect free whenever possible; defer heavy work to asynchronous tasks.
- Never mutate the DOM tree that VoltX currently manages from
beforeMount; wait forafterMountor plugin hooks instead. - When adding analytics or telemetry, remember to remove hooks on navigation or single-page route changes to avoid duplicate events.
- In tests, seed hooks inside the test body and tear them down with the disposer returned from
registerGlobalHookto preserve isolation.
For server-rendered workflows and hydration patterns, refer to ssr.