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.
- the primitives
- function-based Resources
- Lifecycle
- Reactivity
- Example: Clock: Managing own state
- Example:
fetch
: Async + lifecycle
- class-based Resources
- Lifecycle
- Reactivity
- Example: Clock: Managing own state
- Example:
fetch
: Async + lifecycle
- function-based Resources
There are two core abstractions to working with resources, each with their own set of tradeoffs and capabilities -- but ultimately are both summarized as "helpers with optional state and optional cleanup".
class-based Resource |
function-based resource |
|
---|---|---|
supports direct invocation in <templates> |
yes | yes |
supports Glint | soon | soon |
provides a value | the instance of the class is the value1 | can represent a primitive value or complex object2 |
can be invoked with arguments | yes, received via modify 3 hook |
only when wrapped with a function. changes to arguments will cause the resource to teardown and re-run |
persisted state across argument changes | yes | no, but it's possible4 |
can be used in the body of a class component | yes | yes |
can be used in template-only components | yes5 | yes5 |
requires decorator usage (@use ) |
@use optional |
@use optional6 |
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 ceramony 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(() => clearInteral(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(() => clearInteral(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 tracaked 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(() => clearInteral(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';
const Clock = resourceFactory((locale = 'en-US') => {
return resource(({ on }) => {
let time = new TrackedObject({ current: new Date() });
let interval = setInterval(() => (time.current = new Date()), 1_000);
on.cleanup(() => clearInteral(interval))
let formatter = new Intl.DateTimeFormat(locale, { /* ... ✂️ ...*/ });
return () => formatter.format(time.current);
});
});
using <template>
// NOTE: this snippet has bugs and is incomplete, don't copy this (explained later)
import { resource, resourceFactory, use } from 'ember-resources';
const Clock = resourceFactory((locale = 'en-US') => {
return resource(({ on }) => {
let time = new TrackedObject({ current: new Date() });
let interval = setInterval(() => (time.current = new Date()), 1_000);
on.cleanup(() => clearInteral(interval))
let formatter = new Intl.DateTimeFormat(locale, { /* ... ✂️ ...*/ });
return () => formatter.format(time.current);
});
});
<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 becasue 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';
const Clock = resourceFactory((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(() => clearInteral(interval))
let formatter = new Intl.DateTimeFormat(currentLocale, { /* ... ✂️ ...*/ });
return () => formatter.format(time.current);
});
});
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
Class-based resources are good for object-oriented encapsulation of state, giving access to the application container / owner for service injection, and/or persistint state across argument changes.
Though, maybe a more pragmatic approach to the difference:
Class-based resources can be invoked with args. Function-based resources must be wrapped in another function to accept args.
There is only one lifecycle hook, modify
, to encourage data-derivation (via getters) and
generally simpler state-management than you'd otherwise see with with additional lifecycle methods.
For example, this is how you'd handle initial setup, updates, and teardown with a Resource
import { Resource } from 'ember-resources';
import { registerDestructor } from '@ember/destroyable';
class MyResource extends Resource {
// constructor only needed if teardown is needed
constructor(owner) {
super(owner);
registerDestructor(this, () => {
// final teardown, if needed
});
}
modify(positional, named) {
// initial setup, updates, etc
}
}
Many times, however, you may not even need to worry about destruction,
which is partially what makes opting in to having a "destructor" so fun --
you get to choose how much lifecycle your Resource
has.
More info: @ember/destroyable
class-based Resources have lazy, usage-based reactivity based on whatever is accessed in the modify
hook.
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 } from 'ember-resources'; // in V5
import { Resource } from 'ember-resources';
class Doubler extends Resource {
@tracked result = NaN;
modify([num]) {
this.result = num * 2;
}
}
class {
@tracked num = 2;
doubler = Doubler.from(() => [this.num]);
}
When accessed, the value of doubler.result
will be 4
.
Any time this.num
changes, the value of doubler.result
will be 8
.
This happens lazily, so if doubler.result
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).
A class-based Resource can define its own state anywhere, but has the same stipulations
as the function-based Resource: inside the modify
hook, you may not access a tracked
property that is later written to. This causes an infinte loop while the framework tries to resolve what the stable "value" should be.
See the Clock
example below for more details.
Given the complete example of a clock
above implemented in a function-based resource,
A complete implementation, as a class-based resource could look similar to this:
// import { Resource } from 'ember-resources'; // in V5
import { Resource } from 'ember-resources'
import { tracked } from '@glimmer/tracking';
import { registerDestructor } from '@ember/destroyable';
class Clock extends Resource {
@tracked current = new Date();
constructor(owner) {
super(owner);
let interval = setInterval(() => (this.current = new Date()), 1_000);
registerDestructor(this, () => clearInterval(interval));
}
get formatted() {
return this.formatter.format(this.current);
}
modify([locale = 'en-US']) {
this.formatter = new Intl.DateTimeFormat(locale, {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: false,
});
}
}
Resulting usage would look something like this:
Or if you needed the value in JS
class {
clock = Clock.from(this, () => ['en-GB']);
get now() {
return this.clock.formatted;
}
}
See: Cookbook entry, fetch
with AbortController
Footnotes
-
class-based resources cannot be a single primitive value. APIs for support for this have been explored in the past, but it proved unergonomic and fine-grained reactivity per accessed property (when an object was desired for "the value") was not possible. ↩
-
there are alternate ways to shape a function-resource depending on the behavior you want. These shapes and use cases are covered in the function-based Resources. ↩
-
this is the same API / behavior as class-based modifiers in ember-modifier. ↩
-
persisting state across argument changes with function-based resources might require a
WeakMap
and some stable object to reference as the key for the storage within thatWeakMap
. ↩ -
for
.hbs
files the resources will need to be globally available viaexport default
s from theapp/helpers
directory. ↩ ↩2 -
without
@use
, the function-based resource must represent a non-primitive value or object. ↩