
Security News
Deno 2.2 Improves Dependency Management and Expands Node.js Compatibility
Deno 2.2 enhances Node.js compatibility, improves dependency management, adds OpenTelemetry support, and expands linting and task automation for developers.
ember-resources
Advanced tools
An implementation of Resources in Ember.JS without decorators.
This is a V2-format Addon with V1 compatibility
NOTE: if you are also using ember-could-get-used-to-this, @use
is not compatible with
this library's LifecycleResource
, and useResource
does not work with ember-could-get-used-to-this' Resource
.
However, both libraries can still be used in the same project.
npm install ember-resources
# or
yarn add ember-resources
# or
ember install ember-resources
import { useFunction } from 'ember-resources';
class MyClass {
data = useFunction(this, async () => {
let response = await fetch('...');
let json = await response.json();
return json;
}),
}
{{this.data.value}}
useResource
useResource
takes either a Resource
or LifecycleResource
and an args thunk.
import { useResource } from 'ember-resources';
class MyClass {
data = useResource(this, SomeResource, () => [arg list]);
}
When any tracked data in the args thunk is updated, the Resource will be updated as well
The this
is to keep track of destruction -- so when MyClass
is destroyed, all the resources attached to it can also be destroyed.
The resource will do nothing until it is accessed. Meaning, if you have a template that guards access to the data, like:
{{#if this.isModalShowing}}
<Modal>{{this.data.someProperty}}</Modal>
{{/if}}
the Resource will not be instantiated until isModalShowing
is true.
For more info on Thunks, scroll to the bottom of the README
useTask
This is a utility wrapper like useResource
, but can be passed an ember-concurrency task
so that the ember-concurrency task can reactively be re-called whenever args change.
This largely eliminates the need to start concurrency tasks from the constructor, modifiers,
getters, etc.
A concurrency task accessed via useTask
is only invoked when accessed, and automatically updates
when it needs to.
import { useTask } from 'ember-resources';
class MyClass {
myData = useTask(this, this._myTask, () => [args, to, task])
@task
*_myTask(args, to, task) { /* ... */ }
}
Accessing this.myData
will represent the last TaskInstance
, so all the expected properties are available:
value
, isRunning
, isFinished
, etc.
See: the TaskInstance docs for more info.
NOTE: ember-resources
does not have a dependency on ember-concurrency
Resource
Resources extending this base class have no lifecycle hooks to encourage data-derivation (via getters) and generally simpler state-management than you'd otherwise see with the typical lifecycle-hook-aware Resource.
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(owner, args, previous) {
super(owner, args, previous);
if (!previous) {
// initial setup
} else {
// update
}
registerDestructor(this, () => {
// teardown function for each instance
});
}
}
This works much like useFunction
, in that the previous instance is passed to the next instance and there
is no overall persisting instance of MyResource
as the args
update. This technique eliminates the need
to worry about if your methods, properties, and getters might conflict with the base class's API, which is
a common complaint among the anti-class folks.
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
So why even have a class at all?
You may still want to manage state internal to your resource, such as if you were implementing a "bulk selection" resource for use in tabular data. This hypothetical resource may track its own partial / all / none selected state. If the args to this resource change, you get to decide if you want to reset the state, or pass it on to the next instance of the selection resource.
import { Resource } from 'ember-resources';
class Selection extends Resource {
@tracked state = NONE; /* or SOME, ALL, ALL_EXCEPT */
constructor(owner, args, previous) {
super(owner, args, previous);
let { filterQueryString } = args.named;
if (previous && previous.args.named.filterQueryString !== filterQueryString) {
// reset the state when the consumer has changed which records we care about.
this.state = NONE;
}
}
@action selectAll() { this.state = ALL; }
@action deselectAll() { this.state = NONE; }
@action toggleItem(item) { /* ... */ }
// etc
}
usage of this Resource could look like this:
// in either a component or route:
export default class MyComponent extends Component {
@service router;
get filter() {
return this.router.currentRouter.queryParams.filter;
}
// implementation omitted for brevity -- could be passed to EmberTable or similar
records = useResource(this, EmberDataQuery, () => ({ filter: this.filter }));
// the `this.selection.state` property is re-set to NONE when `this.filter` changes
selection = useResource(this, Selection, () => ({ filterQueryString: this.filter }))
}
For library authors, it may be a kindness to consumers of your library to wrap the useResource
call so that they only need to manage one import -- for example:
// addon/index.js
// @private
import { Selection } from './wherever/it/is.js';
// @public
export function useSelection(destroyable, thunk) {
return useResource(destroyable, Selection, thunk);
}
Another example of interacting with previous state may be a "load more" style data loader / pagination:
import { Resource } from 'ember-resources';
import { isDestroyed, isDestroying } from '@ember/destroyable';
class DataLoader extends Resource {
constructor(owner, args, previous) {
super(owner, args, previous);
this.results = previous?.results;
let { url, offset } = this.args.named;
fetch(`${url}?offset=${offset}`)
.then(response => response.json())
.then(results => {
if (isDestroyed(this) || isDestroying(this)) return;
this.results = this.results.concat(result);
});
}
}
consumption of the above resource:
import { useResource } from 'ember-resources';
class MyComponent extends Component {
@tracked offset = 0;
data = useResource(this, DataLoader, () => ({ url: '...', offset: this.offset }));
// when a button is clicked, load the next 50 records
@action loadMore() { this.offset += 50; }
}
LifecycleResource
When possible, you'll want to favor Resource
over LifecycleResource
as Resource
is simpler.
They key differences are that the LifecycleResource
base class has 3 lifecycle hooks
setup
- called upon first access of the resourceupdate
- called when any tracked
used during setup
changesteardown
- called when the containing context is torn downThe main advantage to the LifecycleResource
is that the teardown hook is for "last teardown",
whereas with Resource
, if a destructor is registered in the destructor, there is no way to know
if that destruction is the final destruction.
An example of when you'd want to reach for the LifecycleResource
is when you're managing external long-lived
state that needs a final destruction call, such as with XState, which requires that the "State machine interpreter"
is stopped when you are discarding the parent context (such as a component).
An example
import { LifecycleResource } from 'ember-resources';
import { createMachine, interpret } from 'xstate';
const machine = createMachine(/* ... see XState docs for this function this ... */);
class MyResource extends LifecycleResource {
@tracked state;
setup() {
this.interpreter = interpret(machine).onTransition(state => this.state = state);
}
update() {
this.interpreter.send('ARGS_UPDATED', this.args);
}
teardown() {
this.interpreter.stop();
}
}
Using this Resource is the exact same as Resource
import { useResource } from 'ember-resources';
class ContainingClass {
state = useResource(this, MyResource, () => [...])
}
There is however a semi-unintuitive technique you could use to continue to use Resource
for the final
teardown:
import { Resource } from 'ember-resources';
import { registerDestructor, unregisterDestructior } from '@ember/destroyable';
class MyResource extends Resource {
constructor(owner, args, previous) {
super(owner, args, previous);
registerDestructor(this, this.myFinalCleanup);
if (previous) {
// prevent destruction
unregisterDestructor(prev, prev.myFinalCleanup);
} else {
// setup
}
}
@action myFinalCleanup() { /* ... */ }
}
function
ResourcesWhile functions can be "stateless", Resources don't provide much value unless
you can have state. function
Resources solve this by passing the previous
invocation's return value as an argument to the next time the function is called.
In addition to that state, all function resources inherently track async state for you so you can use plain functions, async or not, in your derived data patterns in you apps and libraries.
There are two flavors of function resources
trackedFunction
useFunction
trackedFunction
This is the simpler of the two function resources, where
Any tracked data accessed in a tracked function before an await
will "entangle" with the function -- we can call these accessed tracked
properties, the "tracked prelude". If any properties within the tracked
payload change, the function will re-run.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { trackedFunction } from 'ember-resources';
class Demo extends Component {
@tracked id = 1;
request = trackedFunction(this, async () => {
let response = await fetch(`https://swapi.dev/api/people/${this.id}`);
let data = await response.json();
return data; // { name: 'Luke Skywalker', ... }
});
updateId = (event) => this.id = event.target.value;
// Renders "Luke Skywalker"
<template>
{{this.request.value.name}}
<input value={{this.id}} {{on 'input' this.updateId}}>
</template>
}
Note, this example uses the proposed <template>
syntax from the First-Class Component Templates RFC
useFunction
Example:
import { useFunction } from 'ember-resources';
class StarWarsInfo {
// access result on info.value
info = useFunction(this, async (state, ...args) => {
if (state) {
let { characters } = state;
return { characters };
}
let [ids] = args;
let response = await fetch(`/characters/${ids}`) ;
let characters = await response.json();
return { characters };
}, () => [this.ids /* defined somewhere */])
}
characters
would be accessed viathis.info.value.characters
in theStarWarsInfo
class
While this example is a bit contrived, hopefully it demonstrates how the state
arg
works. During the first invocation, state
is falsey, allowing the rest of the
function to execute. The next time this.ids
changes, the function will be called
again, except state
will be the { characters }
value during the first invocation,
and the function will return the initial data.
This particular technique could be used to run any async function safely (as long
as the function doesn't interact with this
).
In this example, where the function is async
, the "value" of info.value
is undefined
until the
function completes.
To help prevent accidental async footguns, even if a function is synchronous, it is still ran asynchronously, therefor, the thunk cannot be avoided.
import { useFunction } from 'ember-resources';
class MyClass {
@tracked num = 3;
info = useFunction(this, () => {
return this.num * 2;
});
}
this.info.value
will be undefined
, then 6
and will not change when num
changes.
With the exception of the useResource
+ class
combination, all Thunks are optional.
The main caveat is that if your resources will not update without a thunk -- or consuming
tracked data within setup / initialization (which is done for you with useFunction
).
The args thunk accepts the following data shapes:
() => [an, array]
() => ({ hello: 'there' })
() => ({ named: {...}, positional: [...] })
when an array is passed, inside the Resource, this.args.named
will be empty
and this.args.positional
will contain the result of the thunk.
for function resources, this is the only type of thunk allowed.
when an object is passed where the key named
is not present,
this.args.named
will contain the result of the thunk and this.args.positional
will be empty.
when an object is passed containing either keys: named
or positional
:
this.args.named
will be the value of the result of the thunk's named
propertythis.args.positional
will be the value of the result of the thunk's positional
propertyThis is the same shape of args used throughout Ember's Helpers, Modifiers, etc
These patterns are primarily unexplored so if you run in to any issues, please open a bug report / issue.
Composing class-based resources is expected to "just work", as classes maintain their own state.
import Component from '@glimmer/component';
import { useFunction } from 'ember-resources';
class MyComponent extends Component {
rand = useFunction(this, () => {
return useFunction(this, () => Math.random());
});
}
Accessing the result of Math.random()
would be done via:
{{this.rand.value.value}}
Something to note about composing resources is that if arguments passed to the outer resource change, the inner resources are discarded entirely.
For example, you'll need to manage the inner resource's cache invalidation yourself if you want the inner resource's behavior to be reactive based on outer arguments:
import Component from '@glimmer/component';
import { useFunction } from 'ember-resources';
class MyComponent extends Component {
@tracked id = 1;
@tracked storeName = 'blogs';
records = useFunction(this, (state, storeName) => {
let result: Array<string | undefined> = [];
if (state?.previous?.storeName === storeName) {
return state.previous.innerFunction;
}
let innerFunction = useFunction(this, (prev, id) => {
// pretend we fetched a record using the store service
let newValue = `record:${storeName}-${id}`;
result = [...(prev || []), newValue];
return result;
},
() => [this.id]
);
return new Proxy(innerFunction, {
get(target, key, receiver) {
if (key === 'previous') {
return {
innerFunction,
storeName,
};
}
return Reflect.get(target, key, receiver);
},
});
},
() => [this.storeName]
);
}
{{this.records.value.value}} -- an array of "records"
import type { ArgsWrapper, Named, Positional } from 'ember-resources';
where:
interface ArgsWrapper {
positional?: unknown[];
named?: Record<string, unknown>;
}
this is a utility interface that represents all of the args used throughout Ember.
Example
class MyResource extends LifecycleResource { // default args type
constructor(owner: unknown, args: ArgsWrapper) {
super(owner, args);
}
}
export interface Positional<T extends Array<unknown>> {
positional: T;
}
Example:
class MyResource extends LifecycleResource<Positional<[number]>> {
}
export interface Named<T extends Record<string, unknown>> {
named: T;
}
Example:
class MyResource extends LifecycleResource<Named<{ bananas: number }>> {
}
These shorthands are 3 characters sharter than using the named:
or
positional:
keys that would be required if not using these shorthands...
If your resources are consumed by components, you'll want to continue to test using rendering tests, as things should "just work" with those style of tests.
Where things get interesting is when you want to unit test your resources.
There are two approaches:
new
the resource directlyimport { LifecycleResource } from 'ember-resources';
test('my test', function(assert) {
class MyResource extends LifecycleResource {
// ...
}
let instance = new MyResource(this.owner, { /* args wrapper */ });
// assertions with instance
})
The caveat here is that the setup
and update
functions will have to
be called manually, because we aren't using useResource
, which wraps the
Ember-builtin invokeHelper
, which takes care of reactivity for us. As a
consequence, any changes to the args wrapper will not cause updates to
the resource instance.
For the Resource
base class, there is a static helper method which helps simulate
the update
behavior.
import { Resource } from 'ember-resources';
test ('my test', function (assert) {
class MyResource extends Resource {
// ...
}
let instance = new MyResource(this.owner, { /* args wrapper */ });
let nextInstance = MyResource.next(instance, { /* args wrapper */ });
});
Resource.next
, however, does not destroy the instance. For that, you'll want to use
destroy
from @ember/destroyable
.
import { destroy } from '@ember/destroyable';
// ...
destroy(instance);
If, instead of creating MyResource
directly, like in the example above,
it is wrapped in a test class and utilizes useResource
:
import { useResource } from 'ember-resources';
class TestContext {
data = useResource(this, MyResource, () => { ... })
}
changes to args will trigger calls to setup
and update
.
NOTE: like with all reactivity testing in JS, it's important to
await settled()
after a change to a reactive property so that you allow
time for the framework to propagate changes to all the reactive bits.
Example:
import { LifecycleResource, useResource } from 'ember-resources';
test('my test', async function (assert) {
class Doubler extends LifecycleResource<{ positional: [number] }> {
get num() {
return this.args.positional[0] * 2;
}
}
class Test {
@tracked count = 0;
data = useResource(this, Doubler, () => [this.count]);
}
let foo = new Test();
assert.equal(foo.data.num, 0);
foo.count = 3;
await settled();
assert.equal(foo.data.num, 6);
List of addons that use and wrap ember-resources
to provide more specific functionality:
See the Contributing guide for details.
This project is licensed under the MIT License.
This library wouldn't be possible without the work of:
So much appreciate for the work both you have put in to Resources <3
FAQs
An implementation of Resources with some helpful utilities
The npm package ember-resources receives a total of 17,583 weekly downloads. As such, ember-resources popularity was classified as popular.
We found that ember-resources demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Deno 2.2 enhances Node.js compatibility, improves dependency management, adds OpenTelemetry support, and expands linting and task automation for developers.
Security News
React's CRA deprecation announcement sparked community criticism over framework recommendations, leading to quick updates acknowledging build tools like Vite as valid alternatives.
Security News
Ransomware payment rates hit an all-time low in 2024 as law enforcement crackdowns, stronger defenses, and shifting policies make attacks riskier and less profitable.