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

Reactive Computations

Structured Finalization

Starbeam in Your App

Using @starbeam/react in a React App

Using Starbeam in Libraries

Example: RemoteData

Example: MiniDB

Let's Dig In

ReactiveSubscription