Skip to content

Mark root state. Keep the rest JavaScript.

This walkthrough starts with Starbeam’s smallest useful idea: mark the storage that changes, then build the rest of your model with ordinary JavaScript.

You will write a tiny cart model. The public API will look like normal domain code: cart.items, cart.itemCount, and cart.totalCents. Starbeam makes it reactive because those reads touch marked root state internally.

For this walkthrough, install the framework-neutral package and the reactive collections package:

Terminal window
pnpm add @starbeam/universal @starbeam/collections

Framework apps usually install these packages plus the adapter for their framework, such as @starbeam/react, @starbeam/preact, @starbeam/ember, @starbeam/vue, or @starbeam/svelte. This page stays framework-neutral; framework guides cover the adapter APIs.

Choosing packages for a framework app or reusable library? See Install Starbeam.

The first example uses a reactive Map for root state:

import { reactive } from "@starbeam/collections";

Starbeam lets you define reactive versions of ordinary JavaScript objects and built-in collections. They keep their JavaScript and TypeScript surface: reactive.Map<K, V>() gives you a Map<K, V>, and you use it with the built-in Map API.

Keep root state private and expose the shape your app wants.

import { reactive } from "@starbeam/collections";
interface LineItem {
readonly id: string;
readonly name: string;
readonly priceCents: number;
readonly quantity: number;
}
class Cart {
#items = reactive.Map<string, LineItem>();
get items(): readonly LineItem[] {
return [...this.#items.values()];
}
add(item: LineItem): void {
this.#items.set(item.id, item);
}
get itemCount(): number {
return this.items.reduce((total, item) => total + item.quantity, 0);
}
get totalCents(): number {
return this.items.reduce(
(sum, item) => sum + item.priceCents * item.quantity,
0,
);
}
}

#items is the root state. It is reactive, but it is still a map from string IDs to LineItem values: use set(), values(), get(), has(), and delete().

The rest of the class is ordinary JavaScript. items, add(), itemCount, and totalCents are domain-shaped methods and getters built above the root state.

Collections are the right starting point here because a cart is naturally collection-shaped. If state is object-shaped, use reactive.object(...) instead.

Derived values do not need a wrapper just because they read reactive state. In the cart above, itemCount and totalCents are plain getters.

const cart = new Cart();
cart.totalCents; // 0
cart.add({ id: "tea", name: "Tea", priceCents: 500, quantity: 2 });
cart.itemCount; // 2
cart.totalCents; // 1000

The getters read cart.items. cart.items reads the private collection. Starbeam sees those reads without asking you to wire dependencies by hand.

The consumer of Cart does not need to know about #items.

cart.items;
cart.itemCount;
cart.totalCents;

That is the pattern to prefer: mark root state, then expose domain-shaped JavaScript above it.

Some state needs more than a value. It needs setup, sync, and cleanup.

A Resource keeps the same private-state and domain-shaped-return pattern, but adds lifecycle hooks. This example is a preview; framework adapters normally schedule resources for you.

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

The resource returns an ordinary object with a now property. The lifecycle work is attached to the resource, not pushed through every caller.

Starbeam does not ask an AI agent to translate every domain idea into a reactive DSL before the code can become reactive. The important abstractions stay local, inspectable JavaScript: modules, classes, methods, getters, collections, and explicit lifecycle setup, sync, and cleanup.

The developer still owns the design. Starbeam keeps the reactive boundary small enough that both the human and the agent can reason about the rest of the system as JavaScript.