Starbeam Reactivity
Starbeam is a library for building reactive data systems that integrate natively with UI frameworks such as React, Vue, Svelte or Ember.
It interoperates natively with React state management patterns, Svelte stores, the Vue composition API, and Ember's auto-tracking system.
What is Starbeam Reactivity?
Universal
Starbeam is a write-once, integrate-anywhere library.
When you write code using @starbeam/core
APIs, you can integrate them into any
reactive framework with the subscribe
API.
Starbeam also comes with adapters for React (@starbeam/react
), Vue
(@starbeam/vue
), Svelte (@starbeam/svelte
) and Ember (@starbeam/glimmer
).
These adapters use the subscribe API under the hood to expose idiomatic entry points for each framework.
Integrates Natively
You can use Starbeam in a targeted part of an existing app without needing to change anything else.
Starbeam resources are self-contained, and interact with your framework in a clear, structured way.
That said, when you use multiple Starbeam resources in a single app, Starbeam coordinates with your framework to avoid duplicate work.
Structured Data Flow
Real World -> Hooks (lifetime) -> [Data Land] -> Subscribers (exfiltrating) -> Real World -> browser task or microtask -> repeat
- ResizeObserver
- setInterval
- MousePosition
Data Land
If you remove everything around data land, it makes sense as "Just JS"
Data Cells and Formula Cells
Data Cells
import { reactive } from "@starbeam/core";
const state = reactive({
liters: 0,
});
const increment = () => {
state.liters++;
};
function format(liters: number) {
return new Intl.NumberFormat("en-US", {
style: "unit",
unit: "liter",
unitDisplay: "long",
}).format(liters);
}
format(state.liters); //? "0 liters"
increment();
format(state.liters); //? "1 liter"
increment();
format(state.liters); //? "2 liters"
Formula Cells
import { reactive, formula } from "@starbeam/core";
const description = formula(() => format(state.liters));
subscribe(description, (subscription) => {
console.log(subscription.poll());
});
increment(); // ChangedValue(1)
increment(); // ChangedValue(2)
In UI Frameworks
Making it Universal: Turning it Into a Resource
import { reactive } from "@starbeam/core";
import { formula } from "@starbeam/reactive";
export const LiterCounter = resource(() => {
const state = reactive({ liters: 0 });
const increment = () => {
state.liters++;
};
function format(liters: number) {
return new Intl.NumberFormat("en-US", {
style: "unit",
unit: "liter",
unitDisplay: "long",
}).format(liters);
}
const description = formula(() => format(state.liters));
return {
description,
increment,
};
});
Using a Class
import { cell } from "@starbeam/reactive";
export class LiterCounter {
#liters = cell(0);
increment() {
this.#liters.current++;
}
get description() {
return this.#format(this.#liters.current);
}
#format(liters: number): string {
return new Intl.NumberFormat("en-US", {
style: "unit",
unit: "liter",
unitDisplay: "long",
}).format(liters);
}
}
React
import { LiterCounter } from "#reactive/liter-counter";
import { use } from "@starbeam/react";
export function LiterLikeButton() {
const liters = use(LiterCounter);
return (
<>
<button onClick={() => liters.increment()}>Add a liter</button>
<p>{liters.description}</p>
</>
);
}
Svelte
<script>
import { LiterCounter } from "#reactive/liter-counter";
import { use } from "@starbeam/svelte";
$: liters = use(LiterCounter);
</script>
<button on:click={liters.increment}>Add a liter</button>
<p>{liters.description}</p>
Vue
<script>
import { use } from "@starbeam/vue";
import { LiterCounter } from "#reactive/liter-counter";
export default {
setup() {
const { increment, description } = use(LiterCounter);
return { increment, description };
},
};
</script>
<template>
<button v-on:click="increment">Add a liter</button>
<p>{description}</p>
</template>
Ember
import { use } from "@starbeam/ember";
import { LiterCounter } from "#reactive/liter-counter";
export default class LiterLikeButton extends Component {
readonly liters = use(LiterCounter);
<template>
<button {{on "click" this.liters.increment}}>Add a liter</button>
<p>{{this.liters.description}}</p>
</template>
}
Resources
type RemoteDataState<T> = "loading" | ["data", T] | ["error", unknown];
export function RemoteData<T>(url: string) {
return Resource((resource): Reactive<RemoteDataState<T>> => {
const result = cell("loading" as RemoteDataState<T>);
const controller = new AbortController();
resource.on.cleanup(() => controller.abort());
fetch(url, { signal: controller.signal })
.then((response) => response.json() as Promise<T>)
.then((data) => {
result.set(["data", data]);
})
.catch((error) => {
result.set(["error", error]);
});
return result;
});
}
In UI Frameworks
React
import { use } from "@starbeam/react";
import { RemoteData } from "./remote-data.js";
export default function UserCard({ username }: { username: string }) {
const user = use(
() => RemoteData(`https://api.github.com/users/${username}`),
[username]
);
switch (user.type) {
case "loading":
return <div>Loading...</div>;
case "data":
return (
<div>
<img src={user.data.avatar_url} />
<h1>{user.data.name}</h1>
<p>{user.data.bio}</p>
</div>
);
case "error":
return <div>Error: {user.error.message}</div>;
}
}
Svelte
<script lang="typescript">
import { use } from "@starbeam/svelte";
import { RemoteData } from "./remote-data.js";
export let username: string;
$: user = use(RemoteData(`https://api.github.com/users/${username}`));
</script>
{#if user.type === "loading"}
<div>Loading...</div>
{:else if user.type === "data"}
<div>
<img src={user.data.avatar_url} />
<h1>{user.data.name}</h1>
<p>{user.data.bio}</p>
</div>
{:else if user.type === "error"}
<div>Error: {user.error.message}</div>
{/if}
Vue
<script setup lang="ts">
import { use } from "@starbeam/vue";
import { RemoteData } from "./remote-data.js";
const props = defineProps<{ username: string }>();
const user = use(() => RemoteData(`https://api.github.com/users/${props.username}`));
defineExpose({ user });
</script>
<template>
<div v-if="user.type === 'loading'">Loading...</div>
<div v-if="user.type === 'data'">
<img :src="user.data.avatar_url" />
<h1>{user.data.name}</h1>
<p>{user.data.bio}</p>
</div>
<div v-if="user.type === 'error'">Error: {user.error.message}</div>
</template>
Ember
import { use } from "@starbeam/ember";
import { LiterCounter } from "#reactive/liter-counter";
export default class LiterLikeButton extends Component {
readonly liters = use(LiterCounter);
<template>
<button {{on "click" this.liters.increment}}>Add a liter</button>
<p>{this.liters.description}</p>
</template>
}
Data Universe
With Starbeam reactivity, you describe the states that can change, and computations based on those states automatically update.
Data Coherence: When you change a state, all of the computations derived from that state immediately reflect the change.
import { Cell, reactive } from "@starbeam/core";
const counter = reactive(0);
Counter
import { Cell } from "@starbeam/core";
const counter = Cell(0);
function increment() {
counter.current++;
}
counter.current; //? 0
increment();
counter.current; //? 1