-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(docs): start authoring-resources docs
- Loading branch information
1 parent
9c26e67
commit a6e55af
Showing
1 changed file
with
154 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
# Authoring Resources | ||
|
||
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](#the-primitives) | ||
- [function-based Resources](#function-based-resources) | ||
- [Example: Clock](#example-clock) | ||
- [class-based Resources](#class-based-resources) | ||
|
||
## the primitives | ||
|
||
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`][docs-class-resource] | function-based [`resource`][docs-function-resource] | | ||
| -- | ---------------------- | ------------------------- | | ||
| supports direct invocation in [`<templates>`][rfc-779] | yes | yes | | ||
| supports [Glint][gh-glint] | soon | soon | | ||
| provides a value | the instance of the class is the value[^1] | can represent a primitive value or complex object[^2] | | ||
| 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 possible[^4] | | ||
| can be used in the body of a class component | yes | yes | | ||
| can be used in template-only components | yes[^5] | yes[^5] | | ||
| requires decorator usage (`@use`) | `@use` optional | `@use` optional[^6] | | ||
|
||
|
||
[rfc-779]: https://github.com/emberjs/rfcs/pull/779 | ||
[gh-glint]: https://github.com/typed-ember/glint | ||
[gh-ember-modifier]: https://github.com/ember-modifier/ember-modifier | ||
|
||
[docs-class-resource]: https://ember-resources.pages.dev/classes/core.Resource | ||
[docs-function-resource]: https://ember-resources.pages.dev/modules/util_function_resource#resource | ||
|
||
[mdn-weakmap]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap | ||
|
||
[^1]: 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. | ||
[^2]: 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](#function-based-resources). | ||
[^3]: this is the same API / behavior as class-based modifiers in [ember-modifier][gh-ember-modifier]. | ||
[^4]: persisting state across argument changes with function-based resources might require a [`WeakMap`][mdn-weakmap] and some stable object to reference as the key for the storage within that `WeakMap`. | ||
[^5]: for `.hbs` files the resources will need to be globally available via `export default`s from the `app/helpers` directory. | ||
[^6]: without `@use`, the function-based resource must represent a non-primitive value or object. | ||
|
||
### function-based resources | ||
|
||
[🔝 back to top](#authoring-resources) | ||
|
||
Function resources are good for both authoring encapsulated behaviors, | ||
as well as inline / "on-demand" usage. | ||
|
||
|
||
#### Example: Clock | ||
|
||
[🔝 back to top](#authoring-resources) | ||
|
||
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`][mdn-setInterval] to update a value every second. | ||
```js | ||
// NOTE: this snippet has bugs and is incomplete, don't copy this (explained later) | ||
// import { resource } from 'ember-resources'; // in V5 | ||
import { resource } from 'ember-resources/util/function-resource'; | ||
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 | ||
```hbs | ||
<time>{{clock}}</time> | ||
``` | ||
|
||
But this is not feature-complete! We still need to handle cleanup to prevent memory leaks by using [`clearInterval`][mdn-clearInterval]. | ||
|
||
```diff | ||
- 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`][mdn-DateTimeFormat]. | ||
```diff | ||
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. | ||
```js | ||
const clock = 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('en-US', { | ||
hour: 'numeric', | ||
minute: 'numeric', | ||
second: 'numeric', | ||
hour12: false, | ||
}); | ||
|
||
return () => formatter.format(time.current); | ||
}); | ||
``` | ||
|
||
|
||
[mdn-setInterval]: https://developer.mozilla.org/en-US/docs/Web/API/setInterval | ||
[mdn-clearInterval]: https://developer.mozilla.org/en-US/docs/Web/API/clearInterval | ||
[mdn-DateTimeFormat]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat | ||
|
||
### class-based resources | ||
|
||
[🔝 back to top](#authoring-resources) | ||
|
||
Class | ||
|
||
|