In this document, you'll learn about the core features of ember-resources
and how to decide which primitives to use, how to create, support, compose, and test with them.
- function-based Resources
- Lifecycle
- Reactivity
- Example: Clock: Managing own state
- Example:
fetch
: Async + lifecycle
Function resources are good for both authoring encapsulated behaviors, as well as inline / "on-demand" usage.
The function provided to resource
is synchronous, has no this
, and no way to accept arguments.
But there is a micro-api provided to the function passed to resource
for cleanup.
const myResource = resource(({ on }) => {
// initial setup, updates, etc
on.cleanup(() => {
/* cleanup handled here */
})
return /* some value or synchronous function which returns a value */
});
When a resource is wrapped in a function for the purpose of receiving configurable arguments, the semantics may change slightly
const ArgUsingResource = (someArg) => {
// setup, updates for any time args change
return resource(({ on }) => {
// setup and updates can still be handled here
// resource's cleanup is ran if args change
on.cleanup(() => {
/* cleanup handled here */
})
return /* some value or synchronous function which returns a value */
});
}
Function-based resources are implicitly reactive, in that there is no ceremony required by the consumer to make them reactive or update in response to changes in reactive source-data.
For example, consider a resource that doubles a number (this is over engineered, and you wouldn't want a resource for doubling a number)
import { tracked } from '@glimmer/tracking';
import { resource, use } from 'ember-resources';
class {
@tracked num = 2;
@use doubled = resource(() => this.num * 2);
}
When accessed, the value of doubled
will be 4
.
Any time this.num
changes, the value of doubled
will be a number that is 2 times this.num
.
This happens lazily, so if doubled
is not accessed,
the resource is not evaluated and no computation efforts are done.
Accessing can be done anywhere at any time, in JS, or in a Template (it's the same).
If you wanted your resource
to maintain some state of its own, you'd want to make sure
that you don't invalidate any tracked state that is also consumed in the main function-body of the resource.
import { tracked } from '@glimmer/tracking';
import { resource, use } from 'ember-resources';
import { TrackedObject } from 'tracked-built-ins';
class {
@tracked locale = 'en-US';
@use clock = resource(() => {
let time = new TrackedObject({ current: new Date() });
// changes to locale would invalidate the whole resource, re-invoking the top-level function
let formatter = new Intl.DateTimeFormat(this.locale, { /* ... */ });
// time.current is not accessed in this outer function scope,
// so changing the value does not invalidate the resource body.
setInterval(() => time.current = new Date(), 1_000);
// changes to `time.current` only invalidate this function
return () => formatter.format(time.current);
});
}
For a more in-depth explanation, see the Clock
example below.
Throughout these examples, we'll implement a locale-aware clock and go over the tradeoffs / behavior differences between each of the implementations and usages (from the consuming side).
The goal if this implementation is to provide an easy abstraction that "some consumer" could use to display the current time in their given locale.
To start, we'll want to use setInterval
to update a value every second.
// NOTE: this snippet has bugs and is incomplete, don't copy this (explained later)
import { resource, use } from 'ember-resources';
import { TrackedObject } from 'tracked-built-ins';
const clock = resource(() => {
let time = new TrackedObject({ current: new Date() });
setInterval(() => (time.current = new Date()), 1_000);
return time.current;
});
Usage of this resource would look like
using <template>
// NOTE: this snippet has bugs and is incomplete, don't copy this (explained later)
import { resource, use } from 'ember-resources';
import { TrackedObject } from 'tracked-built-ins';
const clock = resource(() => {
let time = new TrackedObject({ current: new Date() });
setInterval(() => (time.current = new Date()), 1_000);
return time.current;
});
<template>
<time>{{clock}}</time>
</template>
using in a glimmer component or class
class {
@use myClock = clock;
get now() {
return this.myClock; // the formatted time
}
}
But this is not feature-complete! We still need to handle cleanup to prevent memory leaks by using clearInterval
.
- const clock = resource(() => {
+ const clock = resource(({ on }) => {
let time = new TrackedObject({ current: new Date() });
- setInterval(() => (time.current = new Date()), 1_000);
+ let interval = setInterval(() => (time.current = new Date()), 1_000);
+
+ on.cleanup(() => clearInterval(interval))
return time.current;
Now when the resource
updates or is torn down, won't leave a bunch of setInterval
s running.
Lastly, adding in locale-aware formatting with Intl.DateTimeFormat
.
on.cleanup(() => clearInterval(interval))
- return time.current;
+ return new Intl.DateTimeFormat('en-US', {
+ hour: 'numeric',
+ minute: 'numeric',
+ second: 'numeric',
+ hour12: false,
+ }).format(time.current);
However, there is a goofy behavior with this implementation.
By accessing time.current
, we end up consuming tracked data within the resource
callback function. When setInterval
updates time.current
, the reactivity system
detects that "tracked data that was consumed in the resource
callback has changed,
and must re-evaluate".
This causes a new setInterval
and new TrackedObject
to be used,
rather than re-using the objects.
To solve this, we need to enclose access to the tracked data via an arrow function.
const clock = resource(({ on }) => {
let time = new TrackedObject({ current: new Date() });
let interval = setInterval(() => (time.current = new Date()), 1_000);
on.cleanup(() => clearInterval(interval))
- return new Intl.DateTimeFormat('en-US', { /* ... ✂️ ...*/ });
+ let formatter = new Intl.DateTimeFormat('en-US', { /* ... ✂️ ...*/ });
+
+ return () => formatter.format(time.current);
});
In this resource, consumed tracked data, when changed, only invalidates the enclosing function.
Lastly, to support reactively changing the locale, we need to wrap the resource
in a function. Here is the final code:
import { resource, resourceFactory, use } from 'ember-resources';
function Clock(locale = 'en-US') {
return resource(({ on }) => {
let time = new TrackedObject({ current: new Date() });
let interval = setInterval(() => (time.current = new Date()), 1_000);
on.cleanup(() => clearInterval(interval))
let formatter = new Intl.DateTimeFormat(locale, { /* ... ✂️ ...*/ });
return () => formatter.format(time.current);
});
}
resourceFactory(Clock);
using <template>
// NOTE: this snippet has bugs and is incomplete, don't copy this (explained later)
import { resource, resourceFactory, use } from 'ember-resources';
function Clock(locale = 'en-US') {
return resource(({ on }) => {
let time = new TrackedObject({ current: new Date() });
let interval = setInterval(() => (time.current = new Date()), 1_000);
on.cleanup(() => clearInterval(interval))
let formatter = new Intl.DateTimeFormat(locale, { /* ... ✂️ ...*/ });
return () => formatter.format(time.current);
});
}
resourceFactory(Clock);
<template>
<time>{{Clock}}</time>
</template>
Up until now, all we've needed in the template for these clocks to work is to have {{clock}}
in our template.
But because we now need to pass data to a function, we need to invoke that function. The resourceFactory
utility handles some framework-wiring so that the Clock
function can immediately invoke the resource
function.
using in a glimmer component or class
class {
@use clock = Clock('en-GB');
get now() {
return this.clock; // the formatted time
}
}
Supporting reactive argument changes from JS would require an arrow function to be passed to Clock
so that the resource
can consume the entangle with data.
import { resource, resourceFactory, use } from 'ember-resources';
function Clock(locale = 'en-US') {
return resource(({ on }) => {
let currentLocale = locale;
if (typeof locale === 'function') {
currentLocale = locale();
}
let time = new TrackedObject({ current: new Date() });
let interval = setInterval(() => (time.current = new Date()), 1_000);
on.cleanup(() => clearInterval(interval))
let formatter = new Intl.DateTimeFormat(currentLocale, { /* ... ✂️ ...*/ });
return () => formatter.format(time.current);
});
}
resourceFactory(Clock):
and then usage in a class would look like:
class {
@tracked locale = 'en-GB';
@use clock = Clock(() => this.locale);
get now() {
return this.clock; // the formatted time
}
}
See: Cookbook entry, fetch
with AbortController