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.