Skip to content

Latest commit

 

History

History
414 lines (275 loc) · 16.4 KB

resources.md

File metadata and controls

414 lines (275 loc) · 16.4 KB

Resources

  1. Introduction
  2. Resources 👈 You are here
  3. Usage in Ember

this document has been adapted/copied1 from the Starbeam2 documentation

This is a high-level introduction to Resources, and how to use them. For how to integrate Resources in to Ember (Components, etc), see ./ember.md;

In addition to the live demos accompanying each code snippet, all code snippets will have their Starbeam counterparts below them, so that folks can see how similar the libraries are.

When Starbeam is integrated in to Ember, there will be a codemod to convert from ember-resources' APIs to Starbeam's APIs.

details on that soon


Note
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.

Resources 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.

A Very Simple Resource

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

import { cell, resource } from "ember-resources";

export const Now = resource(({ on }) => {
  const now = cell(Date.now());

  const timer = setInterval(() => {
    now.set(Date.now());
  });

  on.cleanup(() => {
    clearInterval(timer);
  });

  return now;
});

To see this code in action, checkout the live demo.

In Starbeam
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 function which returns reactive data -- that depends on reactive state that you created inside the resource constructor.

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

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 state external process cleanup code return value
Cell<number> setInterval clearInterval Cell<number>
Resource's 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.

A Ticking Stopwatch

Here's a demo of a Stopwatch resource, similar to the above demo. The main difference here is that the return value is a function.

import { resource, cell } from 'ember-resources';

const formatter = new Intl.DateTimeFormat("en-US", {
  hour: "numeric",
  minute: "numeric",
  second: "numeric",
  hour12: false,
});

export const Stopwatch = resource((r) => {
  const time = cell(new Date());

  const interval = setInterval(() => {
    time.set(new Date());
  }, 1000);

  r.on.cleanup(() => {
    clearInterval(interval);
  });

  return () => {
    const now = time.current;

    return formatter.format(now);
  };
});

To see this code in action, checkout the live demo.

In Starbeam
import { Cell, Formula, Resource } from "@starbeam/universal";

export const Stopwatch = Resource((r) => {
  const time = Cell(new Date());

  const interval = setInterval(() => {
    time.set(new Date());
  }, 1000);

  r.on.cleanup(() => {
    clearInterval(interval);
  });

  return Formula(() => {
    const now = time.current;

    return new Intl.DateTimeFormat("en-US", {
      hour: "numeric",
      minute: "numeric",
      second: "numeric",
      hour12: false,
    }).format(now);
  });
});

A description of the Stopwatch resource:

internal state external process cleanup code return value
Cell<Date> setInterval clearInterval 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!

const formatter = new Intl.DateTimeFormat("en-US", {
  hour: "numeric",
  minute: "numeric",
  second: "numeric",
  hour12: false,
});

const Stopwatch = resource(({ use }) => {
  const time = use(Now);

  return () => formatter.format(time.current);
});
In Starbeam
const formatter = new Intl.DateTimeFormat("en-US", {
  hour: "numeric",
  minute: "numeric",
  second: "numeric",
  hour12: false,
});

const Stopwatch = Resource(({ use }) => {
  const time = use(Now);

  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.

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".

Compared 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.

import { resourceFactory, resource, cell } from 'ember-resources';

function ChannelResource(channelName) {
  return resource(({ on }) => {
    const lastMessage = cell(null);

    const channel = Channel.subscribe(channelName);

    channel.onMessage((message) => {
      lastMessage.set(message);
    });

    on.cleanup(() => {
      channel.unsubscribe();
    });

    return () => {
      const prefix = `[${channelName}] `;
      if (lastMessage.current === null) {
        return `${prefix} No messages received yet`;
      } /*E1*/ else {
        return `${prefix} ${lastMessage.current}`;
      }
    };
  });
}
resourceFactory(ChannelResource);

To see this code in action, checkout the live demo

In Starbeam
import { Resource, Cell, Formula } from '@starbeam/universal';

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 Ember 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:

flowchart LR
    ChannelResource-->channelName
    subgraph ChannelResource
    lastMessage
    end
    output-->channelName
    output-->lastMessage

    style channelName fill:#8888ff,color:white
    style output fill:#8888ff,color:white
    style lastMessage fill:#8888ff,color:white
Loading

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.

flowchart LR
    ChannelResource-->channelName
    subgraph ChannelResource
    lastMessage
    end
    output-->channelName
    output-->lastMessage

    style channelName fill:#8888ff,color:white
    style output fill:#ff8888,color:black
    style lastMessage fill:#ff8888,color:black
Loading

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.

flowchart LR
    ChannelResource-->channelName
    subgraph ChannelResource
    lastMessage
    end
    output-->channelName
    output-->lastMessage

    style channelName fill:#ff8888,color:black
    style output fill:#ff8888,color:black
    style lastMessage fill:#ff8888,color:black

Loading

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.


Previous: Introduction Next: Usage in Ember

Footnotes

  1. while ~90% of the content is copied, adjustments have been made for casing of APIs, as well as omissions / additions as relevant to the ember ecosystem right now. Starbeam is focused on Universal Reactivity, which in documentation for Ember, we don't need to focus on in this document. Also, mega huge thanks to @wycats who wrote most of this documentation. I, @nullvoxpopuli, am often super stressed by documentation writing (at least when stakes are high) -- I am much happier/relaxed writing code, and getting on the same page between our two projects.

  2. These docs have been adapted from the Starbeam docs on Resources.