Skip to content
On this page

Resources

Key Point

A resource is a reactive function with cleanup logic.

Resources are created with an owner, and whenever the owner is cleaned up, the resource is also cleaned up. This is called ownership linking.

Typically, a component in your framework will own your resources. The framework renderer will make sure that when your component is unmounted, its associated resources are cleaned up.

Deep DiveResources Convert Processes Into Values

Typically, a resource converts an imperative, stateful process, such as an asynchronous request or a ticking timer, into a reactive value.

That allows you to work with a process just like you'd work with any other reactive value.

This is a very powerful capability, because it means that adding cleanup logic to an existing reactive value doesn't change the code that works with the value.

The only thing that changes when you convert a reactive value into a resource is that it must be instantiated with an owner. The owner defines the resource's lifetime. Once you've instantiated a resource, the value behaves like any other reactive value.

In TypeScript, the type of a resource is Reactive<T>, just like a cell or formula.

A Very Simple Resource

To illustrate the concept, let's create a simple resource that represents the current time.

tsx
import { Cell, Resource } from "@starbeam/universal";
 
export const Now = Resource(({ on }) => {
const now = Cell(Date.now());
 
const timer = setInterval(() => {
now.set(Date.now());
});
 
on.cleanup(() => {
clearInterval(timer);
});
 
return now;
});
tsx
import { Cell, Resource } from "@starbeam/universal";
 
export const Now = Resource(({ on }) => {
const now = Cell(Date.now());
 
const timer = setInterval(() => {
now.set(Date.now());
});
 
on.cleanup(() => {
clearInterval(timer);
});
 
return now;
});

A resource's return value is a reactive value. If your resource represents a single cell, it's fine to return it directly. It's also common to return a Formula that depends on reactive state that you created inside the resource constructor.

When you use the Now resource in a component in your framework, it will automatically get its lifetime linked to the component. In this case, that means that the interval will be cleaned up when the component is unmounted.

The Resource function creates a Resource Constructor. A resource constructor:

  1. Sets up internal reactive state that changes over time.
  2. Sets up the external process that needs to be cleaned up.
  3. Registers the cleanup code that will run when the resource is cleaned up.
  4. Returns a reactive value that represents the current state of the resource as a value.

In this case:

internal stateexternal processcleanup codereturn value
Cell<number>setIntervalclearIntervalReactive<number>
Deep DiveResources Values Are Immutable

When you return a reactive value from a resource, it will always behave like a generic, immutable reactive value. This means that if you return a Cell from a resource, the resource's value will have .current and .read(), but not .set(), .update() or other cell-specific methods.

If you want your resource to return a value that can support mutation, you can return a JavaScript object with accessors and methods that can be used to mutate the value.

This is an advanced use-case because you will need to think about how external mutations should affect the running process. Check out the React Query demo in the Starbeam codebase for a good example.

A Ticking Stopwatch

To see how lifetime linking works, here's a simple demo of a Stopwatch resource using the DEBUG_RENDERER.

The code instantiates the stopwatch using an owner it creates locally. When you press the "Finalize the Stopwatch" button, the owner is finalized, which will clean up the stopwatch.

Open on CodeSandboxOpen Sandbox

A description of the Stopwatch resource:

internal stateexternal processcleanup codereturn value
Cell<Date>setIntervalclearIntervalReactive<string>

The internals of the Stopwatch resource behave very similarly to the Now resource. The main difference is that the Stopwatch resource returns the time as a formatted string.

From the perspective of the code that uses the stopwatch, the return value is a normal reactive string.

Reusing the Now Resource in Stopwatch

You might be thinking that Stopwatch reimplements a whole bunch of Now, and you ought to be able to just use Now directly inside of Stopwatch.

You'd be right!

tsx
const Stopwatch = Resource(({ use }) => {
const time = use(Now);
const time: Reactive<number>
 
const formatter = new Intl.DateTimeFormat("en-US", {
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: false,
});
 
return Formula(() => formatter.format(time.current));
});
tsx
const Stopwatch = Resource(({ use }) => {
const time = use(Now);
const time: Reactive<number>
 
const formatter = new Intl.DateTimeFormat("en-US", {
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: false,
});
 
return Formula(() => formatter.format(time.current));
});

The Stopwatch resource instantiated a Now resource using its use method. That automatically links the Now instance to the owner of the Stopwatch, which means that when the component that instantiated the stopwatch is unmounted, the interval will be cleaned up.

Powerful Composition in Universal Code

The use method allows you to create resources that build on other resources in universal code. You can create a composed resource like Stopwatch without locking yourself in to the details of any framework's reactivity system, and then let anyone use it with the Starbeam renderer for their framework.

Powerful stuff!

Using a Resource to Represent an Open Channel

Resources can do more than represent data like a ticking clock. You can use a resource with any long-running process, as long as you can represent it meaningfully as a "current value".

Deep DiveCompared to Other Systems: Destiny of Unused Values

You might be thinking that resources sound a lot like other systems that convert long-running processes into a stream of values (such as observables).

While there are similarities between Resources and stream-based systems, there is an important distinction: because Resources only produce values on demand, they naturally ignore computing values that would never be used.

This includes values that would be superseded before they're used and values that would never be used because the resource was cleaned up before they were demanded.

This means that resources are not appropriate if you need to fully compute values that aren't used by consumers.

In stream-based systems, there are elaborate ways to use scheduling or lazy reducer patterns to get similar behavior. These approaches tend to be hard to understand and difficult to compose, because the rules are in a global scheduler, not the definition of the stream itself. These patterns also give rise to distinctions like "hot" and "cold" observables.

On the other hand, Starbeam Resources naturally avoid computing values that are never used by construction.

TL;DR Starbeam Resources do not represent a stream of values that you operate on using stream operators.

Key Point

Starbeam resources represent a single reactive value that is always up to date when demanded.

This also allows you to use Starbeam resources and other values interchangably in functions, and even pass them to functions that expect reactive values.

Let's take a look at an example of a resource that receives messages on a channel, and returns a string representing the last message it received.

In this example, the channel name that we're subscribing to is dynamic, and we want to unsubscribe from the channel whenever the channel name changes, but not when we get a new message.

tsx
function ChannelResource(
channelName: Reactive<string>,
): ResourceBlueprint<string> {
return Resource(({ on }) => {
const lastMessage = Cell(null as string | null);
 
const channel = Channel.subscribe(channelName.read());
 
channel.onMessage((message) => {
lastMessage.set(message);
});
 
on.cleanup(() => {
channel.unsubscribe();
});
 
return Formula(() => {
const prefix = `[${channelName.read()}] `;
if (lastMessage.current === null) {
return `${prefix} No messages received yet`;
} /*E1*/ else {
return `${prefix} ${lastMessage.current}`;
}
});
});
}
tsx
function ChannelResource(
channelName: Reactive<string>,
): ResourceBlueprint<string> {
return Resource(({ on }) => {
const lastMessage = Cell(null as string | null);
 
const channel = Channel.subscribe(channelName.read());
 
channel.onMessage((message) => {
lastMessage.set(message);
});
 
on.cleanup(() => {
channel.unsubscribe();
});
 
return Formula(() => {
const prefix = `[${channelName.read()}] `;
if (lastMessage.current === null) {
return `${prefix} No messages received yet`;
} /*E1*/ else {
return `${prefix} ${lastMessage.current}`;
}
});
});
}
tsx
function ChannelResource(channelName) {
return Resource(({ on }) => {
const lastMessage = Cell(null);
 
const channel = Channel.subscribe(channelName.read());
 
channel.onMessage((message) => {
lastMessage.set(message);
});
 
on.cleanup(() => {
channel.unsubscribe();
});
 
return Formula(() => {
const prefix = `[${channelName.read()}] `;
if (lastMessage.current === null) {
return `${prefix} No messages received yet`;
} /*E1*/ else {
return `${prefix} ${lastMessage.current}`;
}
});
});
}
tsx
function ChannelResource(channelName) {
return Resource(({ on }) => {
const lastMessage = Cell(null);
 
const channel = Channel.subscribe(channelName.read());
 
channel.onMessage((message) => {
lastMessage.set(message);
});
 
on.cleanup(() => {
channel.unsubscribe();
});
 
return Formula(() => {
const prefix = `[${channelName.read()}] `;
if (lastMessage.current === null) {
return `${prefix} No messages received yet`;
} /*E1*/ else {
return `${prefix} ${lastMessage.current}`;
}
});
});
}

ChannelResource is a JavaScript function that takes the channel name as a reactive input and returns a resource constructor.

That resource constructor starts by subscribing to the current value of the channelName, and then telling starbeam to unsubscribe from the channel when the resource is cleaned up.

It then creates a cell that holds the last message it received on the channel, and returns a function that returns that message as a formatted string (or a helpful message if the channel hasn't received any messages yet).

At this point, let's take a look at the dependencies:

OutputchannelNameChannelResourcelastMessage

Our output depends on the channel name and the last message received on that channel. The lastMessage depends on the channel name as well, and whenever the channel name changes, the resource is cleaned up and the channel is unsubscribed.

If we receive a new message, the lastMessage cell is set to the new message. This invalidates lastMessage and therefore the output as well.

OutputchannelNameChannelResourcelastMessage

However, this does not invalidate the resource itself, so the channel subscription remains active.

On the other hand, if we change the channelName, that invalidates the ChannelResource itself.

OutputchannelNameChannelResourcelastMessage

As a result, the resource will be cleaned up and the channel unsubscribed. After that, the resource will be re-created from the new channelName, and the process will continue.

Key Point

From the perspective of the creator of a resource, the resource represents a stable reactive value.

Under the Hood

Under the hood, the internal ChannelResource instance is cleaned up and recreated whenever its inputs change. However, the resource you got back when you created it remains the same.

That's what makes it possible to pass a resource to a Starbeam renderer and have it continue to work even when the internal resource is torn down and recreated.

Released under the MIT license