zfb
GitHub repository

Type to search...

to open search from anywhere

Scopes and lifecycle

Register setup resources, browser activation, effects, and cleanup with the current component scope.

Get the current owner

Call getScope during synchronous component setup, or inside a Show or For factory, to register work with the owner that is rendering that function. A direct helper call during setup shares the same owner; it does not create a detached scope.

import type { ReadonlySignal } from "@takazudo/zfb/zudo-react";
import { getScope, signal } from "@takazudo/zfb/zudo-react";

function Clock() {
  const scope = getScope();
  const now = signal(Date.now());

  scope.onActivate(() => {
    const timer = window.setInterval(() => {
      now.value = Date.now();
    }, 1000);

    return () => window.clearInterval(timer);
  });

  return <time>{now}</time>;
}

getScope is setup-only. Calling it outside a current render owner throws ZR_NO_SCOPE. No owner survives an await, so async setup is not supported. Pure SDK constructors can be called without a scope because they only create descriptions.

Separate setup from activation

Register synchronous resource cleanup with onCleanup. Register browser-only startup with onActivate; an activation callback may return a synchronous cleanup. Register a tracked browser effect with effect:

function PageTitle({ title }: { title: ReadonlySignal<string> }) {
  const scope = getScope();

  scope.effect(() => {
    document.title = title.value;
  });

  return <h1>{title}</h1>;
}

Activation callbacks run only in the browser, after the DOM has been committed. The renderer assigns every ref before any callback. Child scopes activate before their parents; siblings follow output order, and callbacks in one scope follow registration order. Effects begin in the scheduler's first effect phase after all activation callbacks finish. The server never runs browser activation or effects.

An effect tracks values read during its callback. Before an effect reruns, its prior cleanup runs without tracking. The final cleanup runs when the scope is disposed. A throwing effect is reported, its old dependencies detach, and other effects continue.

If activation fails, the runtime disposes that root's runtime resources and reports the failure. User code that already changed external state must provide its own cleanup; the runtime cannot reverse arbitrary side effects.

Promise-valued cleanup is invalid. Cleanup callbacks are synchronous and must be registered while the scope is active. Callbacks should use the supplied abort signal for later asynchronous work and check it before applying results.

Disposal does not remove DOM

Scope disposal marks the owner inactive, aborts its signal, blocks queued jobs, disposes child scopes, runs cleanup callbacks in reverse registration order while the nodes still exist, and finally clears refs. It continues through cleanup errors and reports them. Disposed work cannot be reactivated.

At a mounted root, dispose releases runtime resources but leaves the wrapper and its DOM in place. unmount disposes and removes the nodes owned by that handle, while retaining the wrapper. zfb uses disposal before replacing a wrapper's body itself. Removing a root or cancelling a pending trigger prevents future activation.

The client root handle exposes hydrate and render outcomes together with its identity and disposed state. hydrate adopts existing nodes; mount creates new child DOM. Either reports expected failures and returns null rather than a partly active handle. zfb isolates root errors, so a broken island does not prevent other roots from being processed.

Server rendering owns a temporary scope graph and always disposes it when the render ends, including on error. Put request-specific mutable state inside rendering rather than in module-level variables.

Local subtree recovery

Use ErrorBoundary around a component that may fail while preparing or rendering its content. It replaces that region with a fallback and keeps siblings mounted and interactive. Returning an inert description inside an ordinary component's try/catch cannot catch a descendant that will execute later in the renderer.

import { ErrorBoundary, signal } from "@takazudo/zfb/zudo-react";

function EditorPreview() {
  const revision = signal(0);

  return (
    <main>
      <ErrorBoundary
        resetKey={revision}
        children={() => <DocumentPreview />}
        fallback={(error, reset) => (
          <button type="button" on:click={() => reset()}>
            Preview failed. Retry
          </button>
        )}
      />
      <EditorToolbar />
    </main>
  );
}

Update revision after changing the preview input, or use the fallback's reset callback. The children and fallback props are factories, consistent with Show's branch ownership. Synchronous setup can use getScope inside either factory. Reset runs a fresh attempt in the commit queue; multiple pending requests coalesce. A resetKey change uses Object.is and resets healthy content as well as failed content. Merely reading signals during setup does not subscribe the boundary to them. Reset callbacks become inert when their boundary is disposed.

The failed attempt's scopes are disposed and detached before rendering the fallback. Cleanup, listeners, refs, subscriptions, and queued work belong to that attempt; retries create fresh resources. The healthy siblings keep their own scopes and DOM. Cleanup errors remain diagnostic and do not prevent the remaining cleanups. Unmount disposes the current attempt or fallback. A fallback renders under the enclosing recovery owner, so errors in it propagate outward. An unhandled error keeps the existing root or scheduler diagnostics and original error detail.

PhaseRecovery behavior
Descendant synchronous component setup, children factory, and ordinary per-node render validationNearest ErrorBoundary
Renderer-scheduled Show/For source reads, key functions, and new branch/item renderingNearest ErrorBoundary
Reactive text, attribute, style, and ordinary rawHtml renderingNearest ErrorBoundary
Boundary retry preparationThis boundary; a failing fallback propagates outward
Aggregate form validation, including cross-control model/radio-group checks and final server radio validationExisting root-owned validation; groups can span boundaries
Event handlers, effects, activation callbacks, cleanup, and asynchronous rejectionsExisting handling; not caught by ErrorBoundary
Island identity, props transport, nested islands, reserved rawHtml, or malformed protocol/range markersIntegrity failure; never hidden by a fallback

A synchronous component returning a promise is an invalid render result; catching that validation error does not observe or recover a later promise rejection. Keep explicit async error handling in the application. Arbitrary external side effects are not reversed by a boundary.

Server rendering discards a failed attempt's partial HTML and restores its renderer bookkeeping before rendering fallback. Hydration always recreates the boundary's interior from the client attempt while adopting surrounding siblings. This includes healthy server/client agreement; form edits and node identity inside the boundary do not survive hydration. See Boundary hydration before placing editable controls inside it.

See also

Revision History

CreatedUpdated