• This repository has been archived on 23/Jun/2022
  • Stars
    star
    269
  • Rank 152,662 (Top 4 %)
  • Language
    TypeScript
  • Created about 3 years ago
  • Updated almost 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

This repo is here as a historical artifact. The up-to-date version of Starbeam is located at https://github.com/starbeamjs/starbeam


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.

Starbeam Reactivity

Starbeam's reactivity is based on a very simple, but powerful, idea:

  • You mark your mutable state as reactive. Individual pieces of mutable state are called data cells.
  • You use normal functions (or getters) to compute values based on your mutable state.
  • You can turn a function into a formula cell to automatically cache it, and it will only recompute when the data cells it uses change.
  • You use resources to compute values that require structured cleanup.

We call this collection of values the data universe. The data universe is always internally coherent. Once you mutate your state, you can call any function that depends on the mutable state, and that function will see an up-to-date version of the state.

Formulas, too, are always up to date. If you change a data cell that a formula depends on, and ask the formula for its current value, the formula will always produce a value that is up to date. You never need to worry about stale data.

The data universe becomes reactive when you plug it into your UI framework. Once you plug it into your UI framework, any changes to the data universe will be reflected in your UI automatically.

πŸ“ Collectively, data cells and formula cells are called cells.

Data Cells and Formulas

const state = reactive({
  inches: 0,
});

const increment = () => {
  state.inches++;
};

const inches = formula(() => {
  return new Intl.NumberFormat(undefined, {
    style: "unit",
    unit: "inch",
  }).format(state.inches);
});

expect(inches.current).toBe("0 inches");

increment();
expect(inches.current).toBe("1 inch");

increment();
expect(inches.current).toBe("2 inches");

Making It Universal

export function InchCounter() {
  const state = reactive({
    inches: 0,
  });

  const increment = () => {
    state.inches++;
  };

  const description = formula(() => {
    return new Intl.NumberFormat(undefined, {
      style: "unit",
      unit: "inch",
    }).format(state.inches);
  });

  return {
    increment,
    description,
  };
}

Plugging it into your UI

React

import { use } from "@starbeam/react";
import { InchCounter } from "#shared";

export function MeasureInches() {
  const inches = use(InchCounter);

  return (
    <>
      <button onClick={inches.increment}>Increment Inches</button>
      <div>{inches.description}</div>
    </>
  );
}

Svelte

<script>
  import { InchCounter } from "#shared";

  $: inches = InchCounter();
</script>

<button on:click={inches.increment}>Increment Inches</button>
<div>{inches.description}</div>

Vue

<script>
import { InchCounter } from "#shared";

export default {
  setup() {
    const inches = InchCounter();

    return {
      inches,
    };
  },
};
</script>

<template>
  <button v-on:click="inches.increment">Increment Inches</button>
  <div>{{ inches.description }}</div>
</template>

Resources

So what is a resource? A resource is a reactive value, just like our InchCounter above, that requires some cleanup. When you use a resource, you link it to an owner object, and when the owner object is cleaned up, the resource will be cleaned up as well. In practice, most of the time, the owner object is a component in your UI framework.

The RemoteData Resource

In this example, we'll create a RemoteData resource that will fetch data from a remote server.

Note: We do not call this the fetch resource, because a resource represents a value not a task with a starting and stopping point. Because of this, the resource is linked, 1:1, to the owner object.

function RemoteData(url) {
  return Resource((resource) => {
    const result = cell({ type: "loading" });

    const controller = new AbortController();
    resource.on.cleanup(() => controller.abort());

    const response = fetch(url, { signal: controller.signal })
      .then((response) => response.json())
      .then((data) => {
        result.set({ type: "data", data });
      })
      .catch((error) => {
        result.set({ type: "error", error });
      });

    return result;
  });
}

Inside of the RemoteData function, we use the Resource function to create a new resource. The Resource constructor takes a function, which we call the "resource constructor". The resource constructor returns a cell that represents its current value. When code uses the resource, its value will be the current value of the reactive value.

A resource constructor is called once, when the resource is first used. A resource constructor:

  • creates internal cells to manage its state
  • connects to any stateful external objects it needs to manage, such as a network connection
  • describes how to disconnect from those external objects when the resource is cleaned up
  • returns a cell that represents the resource's current value

πŸ’‘ A resource can use mutable state internally, and it can interact with the imperative world, but it exposes the messy outside world as a cell that can be used in the data universe like any other cell, including in other formulas and even other resources.

Using it in React

Now that we've defined our data universe, we want to plug it into React to create a reactive system.

import { use } from "@starbeam/react";

function UserCard({ username }: { username: string }) {
  // when `username` changes, we clean up the old `RemoteData` resource and create a new one.
  const user = use(
    () => RemoteData(`https://api.github.com/users/${username}`),
    [username]
  );

  if (user.type === "loading") {
    return <div>Loading...</div>;
  } else if (user.type === "error") {
    return <div>Error: {user.error.message}</div>;
  } else {
    return <div>{user.data.name}</div>;
  }
}

In principle, we could turn RemoteData into a React hook that abstracts the dependencies for once and for all. The useRemoteData hook would take a URL, and whenever the URL changes, it would clean up the old resource and create a new one.

import { use } from "@starbeam/react";

function useRemoteData<T>(url: string) {
  return use(() => RemoteData(url), [url]);
}

And now we can use it in our app:

import { useRemoteData } from "#hooks/remote-data";

function UserCard({ username }: { username: string }) {
  const user = useRemoteData(`https://api.github.com/users/${username}`);

  if (user.type === "loading") {
    return <div>Loading...</div>;
  } else if (user.type === "error") {
    return <div>Error: {user.error.message}</div>;
  } else {
    return <div>{user.data.name}</div>;
  }
}

Using it in Svelte

We can plug the same RemoteData resource into Svelte by turning it into a Svelte store.

<script>
  import { RemoteData } from "./remote-data";
  import { use } from "@starbeam/svelte";

  // username is a prop
  export let username;

  // `use` turns a Starbeam resource into a Svelte store.
  //
  // We use the `$:` syntax so that Svelte automatically unsubscribes from the
  // resource when the username changes and creates a new one.
  $: user = use(RemoteData(`https://api.github.com/users/${username}`));
</script>

{#if $user.type === "loading"}
  <div>Loading...</div>
{:else if $user.type === "error"}
  <div>Error: {user.error.message}</div>
{:else}
  <div>{$user.data.name}</div>
{/if}

Turn Firebase Into a Resource

Next, we'll build a slightly more complicated example that uses Firebase. We'll create a resource for the application, which subscribes to Firebase (and unsubscribes when the application is cleaned up). That app resource will vend Firebase documents as resources, which will be automatically updated when the document changes, and cleaned up when their owner is cleaned up.

Basically, we're using Starbeam reactivity and ownership to manage a lot of the complexity that comes up when subscribing to Firebase documents.

import { initializeApp } from "firebase/app";
import { getDatabase, type Database } from "firebase/database";

const firebaseConfig = {
  apiKey: "AIzaSyB-x-q-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X",
  authDomain: "my-app.firebaseapp.com",
  databaseURL: "https://my-app.firebaseio.com",
  projectId: "my-app",
  storageBucket: "my-app.appspot.com",
  messagingSenderId: "123456789",
  appId: "1:123456789:web:123456789",
};

class Firebase {
  #db: Database;

  constructor(db: Database) {
    this.#db = db;
  }

  at(path: string) {
    return document(this.#db, path);
  }
}

// `firebase` is defined as a generic resource, which means it has properly described setup and cleanup.
//
// It is intended to be used as a service, which would make it a singleton *in the app*, but that means
// that *apps* can be cleaned up, which is very useful in testing and when rendering on the server in a
// shared context.
//
// In short, instead of using module state as a singleton, use a service.
export const firebase = Resource((resource) => {
  const firebaseConfig = {
    apiKey: "AIzaSyB-x-q-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X",
    authDomain: "my-app.firebaseapp.com",
    databaseURL: "https://my-app.firebaseio.com",
    projectId: "my-app",
    storageBucket: "my-app.appspot.com",
    messagingSenderId: "123456789",
    appId: "1:123456789:web:123456789",
  };

  const app = initializeApp(firebaseConfig);
  const database = getDatabase(app);

  resource.on.cleanup(() => database.goOffline());
});

export function document(db: Database, path: string) {
  return Resource((resource) => {
    const firebaseDocument = db.ref(path);

    const document = cell({ type: "loading" });

    firebaseDocument.on("value", (snapshot) => {
      document.set({ type: "data", data: snapshot.val() });
    });

    resource.on.cleanup(() => firebaseDocument.off("value"));

    return () => document.current;
  });
}

Using the Firebase Resource in React

const Document = component(({ path }, starbeam) => {
  const db = starbeam.service(firebase);
  const document = starbeam.use(() => db.at(path.current));

  return () => {
    if (document.current.type === "loading") {
      return <div>Loading...</div>;
    }

    return <div>{document.current.data.name}</div>;
  };
});

Using the Firebase Resource in Svelte

<script lang="typescript">
  import { firebase } from "./firebase";
  import { service } from "@starbeam/svelte";

  export let path: string;

  $: db = service(firebase);
  $: document = use(db.at(path));
</script>

{#if document.type === "loading"}
  <div>Loading...</div>
{:else}
  <div>{document.data.name}</div>
{/if}

Using the Firebase Resource in Ember

import { service, resource } from "@starbeam/ember";
import { firebase } from "./firebase";

export default class extends Component {
  @service(firebase) db;
  @use document = resource(() => this.db.at(this.args.path));

  <template>
    {{#match this.document}}
      {{:when "loading"}}
        <div>Loading...</div>
      {{:when "data" as |user|}}
        <div>{{user.name}}</div>
    {{/match}}
  </template>
}

Using the Firebase Resource in Vue

<script>
import { service, resource } from "@starbeam/vue";
import { firebase } from "./firebase";
export default {
  setup() {
    const db = service(firebase);

    return {
      document: resource(() => db.at(this.args.path)),
    };
  },
};
</script>

<template>
  <div v-if="document.type === 'loading'">Loading...</div>
  <div v-else>{{ document.data.name }}</div>
</template>

Starbeam Element Modifiers

https://github.com/maslianok/react-resize-detector

First, we'll build a simple element modifier that will detect when an element is resized.

interface Size {
  readonly width: number;
  readonly height: number;
}

const ElementSize = Modifier((element, modifier) => {
  const box = element.getBoundingClientRect();
  const size = reactive({ width: box.width, height: box.height });

  const observer = new ResizeObserver((entries) => {
    const last = entries[entries.length - 1];
    size.width = entry.contentRect.width;
    size.height = entry.contentRect.height;
  });

  observer.observe(element);

  modifier.on.cleanup(() => observer.disconnect());

  return size;
});

Using it in React

To see how to use this, let's build a tiny popover library that orients a popover above a target element at its horizontal center.

/**
 * The Popover function takes a content string and padding as options (the options can be reactive,
 * and the popover will update when they change).
 *
 * It returns a Modifier whose value is the rendered popover.
 */
function Popover(options: { content: string; padding?: number }) {
  return Modifier((element, modifier) => {
    const size = ElementSize(element);

    return () => (
      <div
        className="popover"
        style={{ left: size.width / 2, top: -options.padding }}
      >
        {options.content}
      </div>
    );
  });
}

And using it in our app.

function App() {
  return useStarbeam((starbeam) => {
    const options = reactive({ content: "Hello fellow students", padding: 20 });

    // starbeam.render takes a modifier that returns JSX, and returns two values.
    //
    // The first is a ref that you can attach to a DOM element. The second is
    // initially null, but will be set to the result of the modifier when the
    // element is attached.
    const [container, popover] = starbeam.render(Popover(options));

    return () => (
      <div>
        <h1>Hello</h1>
        <article ref={container}>
          {popover}
          Hello world. This is my first blog post. I'm so proud of myself.
        </article>
      </div>
    );
  });
}

Using it in Svelte

The same popover library, but for svelte:

// popover.svelte

<script>
  import ElementSize from "./ElementSize";

  export let container;
  export let padding = 20;

  // use turns a Starbeam resource into a Svelte store.
  const size = use(ElementSize(container));
</script>

<div
  class="popover"
  style={{ left: $size.width / 2, top: -padding }}
>
  <slot />
</div>
<script>
  import ElementSize from "./ElementSize";

  let container;
</script>

<div>
  <h1>Hello</h1>
  <article bind:this={container}>
    {#if container}
      <Popover {container}>Hello fellow students</Popover>
    {/if}
    Hello world. This is my first blog post. I'm so proud of myself.
  </article>
</div>

Using Starbeam to Define React Components

So far we've been using Starbeam to define resources and then using them in React. But what if we want to define a React component that uses Starbeam directly?

We're going to take an example that Jack Herrington used in his video "Mastering React's useEffect" and see how to model the same thing using Starbeam.

https://github.com/jherr/taming-useeffect/blob/main/src/App.js

function RemoteData(url, { onSuccess }: { onSuccess: Reactive<() => void> }) {
  return Resource((resource) => {
    const result = cell({ type: "loading" });

    const controller = new AbortController();
    resource.on.cleanup(() => controller.abort());

    const response = fetch(url, { signal: controller.signal })
      .then((response) => response.json())
      .then((data) => {
        onSuccess.current();
        result.set({ type: "data", data });
      })
      .catch((error) => {
        result.set({ type: "error", error });
      });

    return result;
  });
}
import { useState, useEffect } from "react";
import "./App.css";

import RemoteData from "./RemoteData";

function App() {
  return useResource((resource) => {
    const count = resource.use(Stopwatch);
    const state = reactive({ user: "jack" });
    const user = resource.use(() => RemoteData(`/${state.user}.json`));

    return () => (
      <div className="App">
        <div>Hello</div>
        <div>Count: {count}</div>
        <div>{JSON.stringify(user)}</div>
        <div>
          <button onClick={() => (state.user = "jack")}>Jack</button>
          <button onClick={() => (state.user = "sally")}>Sally</button>
        </div>
      </div>
    );
  });
}

const Stopwatch = Resource((resource) => {
  const counter = reactive({ count: 0 });

  const interval = setInterval(() => {
    counter.count++;
  }, 1000);

  resource.on.cleanup(() => clearInterval(interval));

  return () => counter.count;
});

export default App;

Audience

Who is the audience of this README? Here are some audiences:

  • Ember users interested in seeing what's going on here.
  • People wanting to build portable libraries that work reactively in multiple UI frameworks without having to understand the details of each framework's reactivity systems.
  • People wanting a more ergonomic and universal reactivity system that works well in their existing UI framework.

Key words:

  • portable
  • hooks
  • reactive
  • resources
  • formulas

More Repositories

1

javascript-decorators

2,397
star
2

jquery-offline

A jQuery plugin to facilitate conveniently working with local storage
JavaScript
836
star
3

rack-offline

A Rack and Rails plugin for building offline web applications
Ruby
670
star
4

moneta

a unified interface to key/value stores
Ruby
476
star
5

merb-core

Merb Core: All you need. None you don't.
Ruby
437
star
6

bundler

Ruby
408
star
7

merb

master merb branch
Ruby
339
star
8

artifice

Replaces Net::HTTP with a subclass that routes all requests to a Rack application
Ruby
216
star
9

merb-plugins

Merb Plugins: Even more modules to hook up your Merb installation
JavaScript
208
star
10

merb-more

Merb More: The Full Stack. Take what you need; leave what you don't.
187
star
11

minispade

JavaScript
175
star
12

textmate

Command-line package manager for textmate
Ruby
160
star
13

rust-activesupport

Small demos that illustrate Rust's expressiveness
Rust
132
star
14

rake-pipeline-web-filters

Ruby
116
star
15

newgem-template

A basic template for building a brand new gem
Ruby
106
star
16

guides

Build your own Rails guides
JavaScript
92
star
17

javascript-private-state

A proposal for private state in JavaScript (Stage 0)
92
star
18

jspec

A JavaScript BDD Testing Library
JavaScript
83
star
19

mio-book

62
star
20

dbmonster

A demo of dbmon running on the idempotent-rerender branches of Ember and HTMLBars
JavaScript
61
star
21

handlebars-site

HTML
56
star
22

net-http

Ruby
43
star
23

rust-civet

A Rust web server
Rust
43
star
24

osx-window-sizing

AppleScripts to resize windows simply
42
star
25

js_module_transpiler

JS Module Transpiler is an experimental compiler that allows you to write your JavaScript using a subset of the current ES6 module syntax, and compile it into AMD modules (and soon, CommonJS modules)
Ruby
42
star
26

irb2

Ruby
37
star
27

hammer.rs

An option parsing library that deserializes flags into structs
Rust
37
star
28

asdf

Make the current directory available on port 9292
Ruby
35
star
29

parsejs

JavaScript
34
star
30

railsnatra

Ruby
32
star
31

benchwarmer

Prettier Benchmarking for Ruby
Ruby
32
star
32

muse

A library that can create HTML or PDF books from an extended Markdown format
Ruby
29
star
33

rust-bridge

Ruby
27
star
34

rust-arel

An in-progress port of the Ruby SQL building library arel
Rust
25
star
35

vigilo

A lightweight, simple API for watching the file system
Ruby
25
star
36

rails_assets

temporary home of Rails 3.1 assets until it's moved into master (in short order)
Ruby
24
star
37

dm-adapters

DataMapper Adapters
Ruby
22
star
38

ember-next-experiments

Shell
19
star
39

merb-extlib

Ruby core extensions library extracted from Merb core.
Ruby
19
star
40

experimental-amber-todos

The SproutCore todos tutorial using the single-file alpha Amber distribution
JavaScript
18
star
41

looper

Static analysis tools for ES6
JavaScript
16
star
42

rufo

a Ruby bridge to flying saucer
Ruby
16
star
43

ruby-spidermonkey

A Ruby Binding to Spidermonkey
C
15
star
44

shimmer

An experimental attempt to restructure Glimmer's concepts around a smaller set of primitives.
TypeScript
15
star
45

jquery-governance

Ruby
15
star
46

language-reporting

Rust
13
star
47

argon

Rust
13
star
48

chainable_compare

Ruby
12
star
49

net2-reactor

Ruby
12
star
50

hbs-parser-next

Messing around with a combinator-based approach to parser while stuck at home
TypeScript
12
star
51

prometheus

11
star
52

everafter

An experiment with making the Glimmer reactivity system more general
TypeScript
11
star
53

github-issues-demo

The code for the Github Issues demo I presented at EmberCamp UK
CSS
11
star
54

alexandria

Ruby bindings to the Google Data API
Ruby
11
star
55

rails-simple-benches

Some Rails benches that work across Rails versions and can be used to compare perf progress
Ruby
10
star
56

html5-parser

JavaScript
10
star
57

Unix.rb

C
10
star
58

sparta

Javascript running on Rubinius VM
Ruby
10
star
59

polaris-sketchwork

A collection of proposals that I (@wycats) am working on fleshing out for inclusion in Polaris.
JavaScript
10
star
60

rails-extension-tutorial

Ruby
9
star
61

rails-benches

Application benchmarks to confirm that optimization impact real-world performance
9
star
62

merb_dm_rest

A HTTP DataMapper bridge served via Merb
Ruby
9
star
63

syntax-highlighter

A web syntax highlighter to make it easy to write code to be pasted into keynote
JavaScript
9
star
64

nnhistory

HTML
9
star
65

rails2_on_rails3

Ruby
9
star
66

abbot-from-scratch

Ruby
9
star
67

jetty-rb

Ruby
9
star
68

allocation_counter

Ruby
8
star
69

active_params

8
star
70

software-patent-petition

8
star
71

w3c-dom

A W3C-compliant DOM written on top of libxml2 (very early stages)
Ruby
8
star
72

jsmodules

CSS
8
star
73

sproutcore-chrome-extension

A Chrome DevTools extension for SproutCore
JavaScript
8
star
74

indexeddb-experiment

This is an experiment building an IndexedDB adapter for Ember. It is very experimental and is tracking changes we are making in ember-data, so it is also unstable.
JavaScript
8
star
75

proc-macro-workshop

Rust
8
star
76

ember-future

Some experiments
JavaScript
7
star
77

rails-api

Ruby
7
star
78

cargo-website

CSS
7
star
79

jquery-ui-sproutcore20

JavaScript
7
star
80

modularity_olympics

My entry to Nick Kallen's modularity olympics (using the Middleware pattern as seen in Rack)
Ruby
7
star
81

simple-callbacks-refactor

Quick demo of how to make callbacks faster
Ruby
7
star
82

joy-ide

An IDE inspired by Rich Kilmer's Talk at Gogaruco
6
star
83

routed-http.rs

Combining rust-http with route-recognizer.rs to create routable HTTP
Rust
6
star
84

rake-pipeline-rails

Ruby
6
star
85

slicehub

An application for managing slices
6
star
86

wand

Rust
5
star
87

core-storage

TypeScript
5
star
88

merb-docs

Ruby
5
star
89

railsconf-rust-demo

Rust
5
star
90

sproutcore-interactive-tutorials

JavaScript
5
star
91

js_tabs_example

An example (not production code!) of how to write evented JS using tabs as an example
JavaScript
5
star
92

rdoc

rdoc fork to support arbitrary API level filtering
5
star
93

laszlo_post_api

Laszlo POST-API
Java
5
star
94

rust-experiments

Rust
5
star
95

rubygems-bug-demo

Ruby
5
star
96

handlebars-parser

An experimental rewrite of the Handlebars parser using Jison
JavaScript
5
star
97

at-media

The code from my @media talk
4
star
98

gkc

Rust
4
star
99

stalkr

Ruby
4
star
100

css_to_xpath

Extracted from Nokogiri in anticipation of merging into Merb
Ruby
4
star