Skip to content
On this page

Our Guiding Principle

We believe that reactive programming should feel like regular programming.

Starbeam reactivity looks and feels like normal JavaScript programming.

You store state in reactive variables and use functions to compute values based on the state.

You can decompose your functions into more functions, just like you would in normal JavaScript, and none of those functions ever need to know that they're reactive.

You can also use classes to organize your reactive state and functions, and even use private fields and accessors.

In Starbeam, any computation based on reactive variables is reactive, no matter how many abstractions are between the reactive variable and the reactive output.

A Quick Demonstration

Let's start with a simple example of reactive programming in Starbeam. It has reactive variables, a reactive function, and renders a reactive output.

tsx
import { Cell, Reactive } from "@starbeam/universal";
 
// `Cell` is the most basic kind of reactive variable.
const person = Cell("Katie Gengler"); // [!code ann]
const affiliation = Cell("EmberObserver");
 
// `card` is a regular function.
function card(
person: Reactive<string>,
affiliation: Reactive<string>,
) {
return `${person.current} (${affiliation.current})`;
}
 
// The `render` function is a placeholder for the Starbeam
// renderer for your framework.
render(() => card(person, affiliation));
tsx
import { Cell, Reactive } from "@starbeam/universal";
 
// `Cell` is the most basic kind of reactive variable.
const person = Cell("Katie Gengler"); // [!code ann]
const affiliation = Cell("EmberObserver");
 
// `card` is a regular function.
function card(
person: Reactive<string>,
affiliation: Reactive<string>,
) {
return `${person.current} (${affiliation.current})`;
}
 
// The `render` function is a placeholder for the Starbeam
// renderer for your framework.
render(() => card(person, affiliation));
tsx
import { Cell } from "@starbeam/universal";
 
// `Cell` is the most basic kind of reactive variable.
const person = Cell("Katie Gengler"); // [!code ann]
const affiliation = Cell("EmberObserver");
 
// `card` is a regular function.
function card(person, affiliation) {
return `${person.current} (${affiliation.current})`;
}
 
// The `render` function is a placeholder for the Starbeam
// renderer for your framework.
render(() => card(person, affiliation));
tsx
import { Cell } from "@starbeam/universal";
 
// `Cell` is the most basic kind of reactive variable.
const person = Cell("Katie Gengler"); // [!code ann]
const affiliation = Cell("EmberObserver");
 
// `card` is a regular function.
function card(person, affiliation) {
return `${person.current} (${affiliation.current})`;
}
 
// The `render` function is a placeholder for the Starbeam
// renderer for your framework.
render(() => card(person, affiliation));

To push the envelope, let's refactor the example to use classes and private fields.

tsx
class Person {
#name: Cell<string>;
#affiliation: Cell<string>;
 
constructor(name: string, affiliation: string) {
this.#name = Cell(name);
this.#affiliation = Cell(affiliation);
}
 
get card(): string {
return `${this.#name.current} (${
this.#affiliation.current
})`;
}
 
set name(name: string) {
this.#name.set(name);
}
 
set affiliation(affiliation: string) {
this.#affiliation.set(affiliation);
}
}
 
const person = new Person("Katie Gengler", "EmberObserver");
 
render(() => person.card);
tsx
class Person {
#name: Cell<string>;
#affiliation: Cell<string>;
 
constructor(name: string, affiliation: string) {
this.#name = Cell(name);
this.#affiliation = Cell(affiliation);
}
 
get card(): string {
return `${this.#name.current} (${
this.#affiliation.current
})`;
}
 
set name(name: string) {
this.#name.set(name);
}
 
set affiliation(affiliation: string) {
this.#affiliation.set(affiliation);
}
}
 
const person = new Person("Katie Gengler", "EmberObserver");
 
render(() => person.card);
tsx
class Person {
#name;
#affiliation;
 
constructor(name, affiliation) {
this.#name = Cell(name);
this.#affiliation = Cell(affiliation);
}
 
get card() {
return `${this.#name.current} (${
this.#affiliation.current
})`;
}
 
set name(name) {
this.#name.set(name);
}
 
set affiliation(affiliation) {
this.#affiliation.set(affiliation);
}
}
 
const person = new Person("Katie Gengler", "EmberObserver");
 
render(() => person.card);
tsx
class Person {
#name;
#affiliation;
 
constructor(name, affiliation) {
this.#name = Cell(name);
this.#affiliation = Cell(affiliation);
}
 
get card() {
return `${this.#name.current} (${
this.#affiliation.current
})`;
}
 
set name(name) {
this.#name.set(name);
}
 
set affiliation(affiliation) {
this.#affiliation.set(affiliation);
}
}
 
const person = new Person("Katie Gengler", "EmberObserver");
 
render(() => person.card);

Even though the two cells are now stored in private fields, and the value is returned from a getter, the rendered reactive output will still update when the cells change.

A real-world implementation using decorators

This quick demonstration used cells and manual getters to make it clear that there's no Starbeam-specific magic in the fields or getters that makes the rendered output reactive.

In practice, you would probably write the Person class using JavaScript decorators:

tsx
class Person {
@reactive #name: string;
@reactive #affiliation: string;
 
constructor(name: string, affiliation: string) {
this.#name = name;
this.#affiliation = affiliation;
}
 
get card(): string {
return `${this.#name} (${this.#affiliation})`;
}
 
set name(name: string) {
this.#name = name;
}
 
set affiliation(affiliation: string) {
this.#affiliation = affiliation;
}
}
 
const person = new Person("Katie Gengler", "EmberObserver");
 
render(() => person.card);
tsx
class Person {
@reactive #name: string;
@reactive #affiliation: string;
 
constructor(name: string, affiliation: string) {
this.#name = name;
this.#affiliation = affiliation;
}
 
get card(): string {
return `${this.#name} (${this.#affiliation})`;
}
 
set name(name: string) {
this.#name = name;
}
 
set affiliation(affiliation: string) {
this.#affiliation = affiliation;
}
}
 
const person = new Person("Katie Gengler", "EmberObserver");
 
render(() => person.card);
tsx
class Person {
@reactive #name;
@reactive #affiliation;
 
constructor(name, affiliation) {
this.#name = name;
this.#affiliation = affiliation;
}
 
get card() {
return `${this.#name} (${this.#affiliation})`;
}
 
set name(name) {
this.#name = name;
}
 
set affiliation(affiliation) {
this.#affiliation = affiliation;
}
}
 
const person = new Person("Katie Gengler", "EmberObserver");
 
render(() => person.card);
tsx
class Person {
@reactive #name;
@reactive #affiliation;
 
constructor(name, affiliation) {
this.#name = name;
this.#affiliation = affiliation;
}
 
get card() {
return `${this.#name} (${this.#affiliation})`;
}
 
set name(name) {
this.#name = name;
}
 
set affiliation(affiliation) {
this.#affiliation = affiliation;
}
}
 
const person = new Person("Katie Gengler", "EmberObserver");
 
render(() => person.card);

Some of the Benefits

Here are some examples of how Starbeam's principles work in practice, especially in ways that might be different from other reactive frameworks you're familiar with.

Data Updates Happen Immediately

When you update a reactive value, the reactive update happens immediately. Any code that accesses the reactive value will see the new value.

There are no exceptions.

This means that you can write elaborate abstractions or libraries that are built on reactive values, and they will behave exactly as you expect regardless of how they're used by app code.

You Derive State Using Normal Functions

If you want to compute a value from reactive values, you just use functions.

You can also use getters, methods, and the new private versions of those features to access the reactive values. You can mix and match JavaScript features however you want. Once you've used a reactive value to store your state, you don't have to think about reactivity as you compute values.

A Longer Demonstration Using Reactive Arrays

This "regular programming" principle goes far beyond single values.

When your reactive code needs to work with collections, like arrays, maps and sets, Starbeam's design is based on the same guiding principle: you can use reactive arrays, maps and sets, and then read from those collections using normal JavaScript.

In this longer demonstration, we'll create a People class that holds multiple people in a reactive array.

tsx
import { reactive } from "@starbeam/js";
 
export class People {
#people: Person[] = reactive.array([]);
 
add(name: string, location: string) {
this.#people.push({ name, location });
}
 
byLocation(location: string) {
return this.#people.filter(
(person) => person.location === location,
);
}
 
update(name: string, location: string) {
const index = this.#people.findIndex(
(person) => person.name === name,
);
 
if (index !== -1) {
this.#people[index] = { name, location };
}
}
}
 
interface Person {
name: string;
location: string;
}
tsx
import { reactive } from "@starbeam/js";
 
export class People {
#people: Person[] = reactive.array([]);
 
add(name: string, location: string) {
this.#people.push({ name, location });
}
 
byLocation(location: string) {
return this.#people.filter(
(person) => person.location === location,
);
}
 
update(name: string, location: string) {
const index = this.#people.findIndex(
(person) => person.name === name,
);
 
if (index !== -1) {
this.#people[index] = { name, location };
}
}
}
 
interface Person {
name: string;
location: string;
}
tsx
import { reactive } from "@starbeam/js";
 
export class People {
#people = reactive.array([]);
 
add(name, location) {
this.#people.push({ name, location });
}
 
byLocation(location) {
return this.#people.filter(
(person) => person.location === location,
);
}
 
update(name, location) {
const index = this.#people.findIndex(
(person) => person.name === name,
);
 
if (index !== -1) {
this.#people[index] = { name, location };
}
}
}
tsx
import { reactive } from "@starbeam/js";
 
export class People {
#people = reactive.array([]);
 
add(name, location) {
this.#people.push({ name, location });
}
 
byLocation(location) {
return this.#people.filter(
(person) => person.location === location,
);
}
 
update(name, location) {
const index = this.#people.findIndex(
(person) => person.name === name,
);
 
if (index !== -1) {
this.#people[index] = { name, location };
}
}
}

As before, we used a private field to store our reactive state, and created methods for adding more items to the array and querying the array.

We could have stored it some other way (like a public field or even in a WeakMap) and everything would have worked just as well.

We created a byLocation method that uses a normal JavaScript filter function to filter the array by location.

The update method uses the somewhat obscure findIndex method to find the person we're updating, and updated the array by replacing the item at that index.

Other than defining #people as a reactive.array, the rest of the People class looks and feels like a normal, encapsulated JavaScript class.

If we then render the result of byLocation into a reactive output, the renderer will update the output whenever update is called.

The bottom line is: While Starbeam's reactive values and rendering concept may feel analogous to the reactive systems you're used to, the similarities end with those concepts. All other reads and writes to those reactive values are normal JavaScript.

Released under the MIT license