Skip to content

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:

EventPosition
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
ts
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 unregisterGlobalHook when the callback is no longer needed.
  • Call clearGlobalHooks("beforeMount") or clearAllGlobalHooks() 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:

ts
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:

ts
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.onMount and ctx.lifecycle.onUnmount let you coordinate DOM state with the binding's lifetime.
  • Use ctx.lifecycle.beforeBinding and ctx.lifecycle.afterBinding to measure binding creation or guard against duplicate initialization.
  • Always combine lifecycle hooks with ctx.addCleanup if 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 for afterMount or 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 registerGlobalHook to preserve isolation.

For server-rendered workflows and hydration patterns, refer to ssr.