Skip to content

Resources and lifecycle

A resource is Starbeam’s lifecycle abstraction. Use root state for values that only need to be read and updated. Use a resource when the work itself needs to start, synchronize, and stop.

That distinction keeps the first idea small:

  1. Mark root state.
  2. Keep derived state as ordinary JavaScript.
  3. Reach for resources when the work has a lifetime.

Plain root state is reactive as soon as you create it. There is no app container, provider, or owner just to make a value reactive.

import { reactive } from "@starbeam/collections";
const count = reactive.object({ value: 0 });
count.value++;
count.value;

The owner appears when the work itself has a lifetime: a timer, subscription, socket, observer, DOM element, or shared app service.

A resource wraps lifecycle work while returning the same kind of value your app would otherwise use. If the returned value already has the right shape, return a reactive object directly.

import { reactive } from "@starbeam/collections";
import { Resource } from "@starbeam/universal";
export const Clock = Resource(({ on }) => {
const clock = reactive.object({ now: new Date() });
on.sync(() => {
clock.now = new Date();
const timer = setInterval(() => {
clock.now = new Date();
}, 1000);
return () => clearInterval(timer);
});
return clock;
});

The resource owns the timer and returns the value the app wants to read: clock.now. The first sync sets clock.now after the framework owner is ready. Later timer ticks update the same reactive object.

If the resource needs derived domain values, expose them with ordinary getters on the returned object.

Resources separate the value you return from the work needed to keep that value connected to the outside world.

  • Setup creates the stable value the resource returns.
  • Sync starts or refreshes external work after the owner is ready.
  • Sync cleanup stops work from the previous sync before the next sync runs.
  • Finalize runs lower-level owning-scope finalizers.

on.sync() registers the sync work. Starbeam runs it when the framework adapter schedules the resource. If the sync callback returns a function, Starbeam runs that cleanup before the next sync and again when the owner finalizes.

That is why the timer cleanup belongs inside on.sync(): each sync owns the timer it created.

Use the same pattern for subscriptions, sockets, observers, and other external work. Start the external work in on.sync() and return the cleanup from that sync.

import { reactive } from "@starbeam/collections";
import { Resource } from "@starbeam/universal";
const Connection = Resource(({ on }) => {
const connection = reactive.object({ status: "idle" });
on.sync(() => {
connection.status = "connecting";
const socket = new WebSocket("wss://example.com");
const onOpen = () => {
connection.status = "open";
};
socket.addEventListener("open", onOpen);
return () => {
socket.removeEventListener("open", onOpen);
socket.close();
connection.status = "closed";
};
});
return connection;
});

on.lowLevel.finalize() is lower-level. It registers work for the owning Starbeam scope’s finalization, not for each sync. Use it when you are working with Starbeam-owned scopes or integration machinery. It is not a replacement for the cleanup returned from on.sync().

Most app code does not call sync or low-level finalization APIs directly. Framework adapters attach resources to framework lifetimes and schedule the work for you.

Framework adapters connect resources to framework lifetimes

Section titled “Framework adapters connect resources to framework lifetimes”

The resource definition is framework-neutral. The adapter decides how setup, sync, cleanup, and finalize fit into the framework that owns the component.

  • React uses useResource(). The resource belongs to the React component; sync runs in React effect timing after commit, and finalize runs when React unmounts the component.
  • Preact uses useResource(). The resource belongs to the Preact component; sync is scheduled through Preact effects, and finalize runs when the component unmounts.
  • Ember uses setupResource(). The resource belongs to an Ember destroyable; sync is scheduled from Starbeam invalidations, and finalize runs when the destroyable is torn down.
  • Vue uses setupResource(). The resource is created from Vue setup, sync is scheduled through Vue’s component lifecycle, and finalize runs when Vue unmounts the component.
  • Svelte supports Starbeam reads and element resources today. It does not expose component-resource or service helpers yet.

This is the reason the framework guides matter. A resource is portable, but the owner is framework-specific. The guide for each adapter shows how Starbeam’s resource phases line up with that framework’s words for component work.

A service is resource-backed state with an app lifetime. Use services for shared application concerns that should live with the app or framework root, not with a single component.

The resource still returns a domain-shaped value. The difference is ownership: the app owns the service lifetime. See Services and app lifetime for the app-facing service model.

Element resources attach work to DOM elements

Section titled “Element resources attach work to DOM elements”

Some resources need a DOM element. Element resources let the framework provide the element while Starbeam owns the setup, sync, and cleanup work.

The concept stays the same: the element comes from the framework, and the value you read stays domain-shaped. See Element resources and DOM attachment for the framework dialects and the stable element-resource shape.

The app-author path is:

  1. Mark root state.
  2. Keep derived state as ordinary JavaScript.
  3. Use a resource when work needs setup, sync, or cleanup.
  4. Use a service when resource-backed state should live with the app.
  5. Use an element resource when the resource needs a DOM element from a framework.

You do not need a resource for every reactive value. Use one when the work has a lifetime.