diff --git a/.changeset/wet-cobras-happen.md b/.changeset/wet-cobras-happen.md new file mode 100644 index 000000000..b472480e8 --- /dev/null +++ b/.changeset/wet-cobras-happen.md @@ -0,0 +1,133 @@ +--- +"ember-resources": minor +--- + +`trackedFunction` can now be composed just like regular resources. + +The function body still auto-tracks and will update within consuming resource appropriately. + +
Example + +```ts +const Person = resourceFactory((maybeIdFn) => { + return resource(({ use }) => { + let request = use( + trackedFunction(async () => { + let id = typeof maybeIdFn === "function" ? maybeIdFn() : maybeIdFn; + let response = await fetch(`https://github.com/gitapi/users/${id}`); + return response.json(); + }) + ); + + // `use` returns a ReadonlyCell where `.current` + // is the State of trackedFunction. + return () => request.current; + }); +}); +``` + +Usage examples: + +```gjs + +``` + +
+ +
An async doubler + +```ts +const Doubled = resourceFactory((num: number) => { + return resource(({ use }) => { + let doubler = use(trackedFunction(async () => num * 2)); + + // Since current is the "State" of `trackedFunction`, + // accessing .value on it means that the overall value of + // `Doubled` is the eventual return value of the `trackedFunction` + return () => doubler.current.value; + }); +}); + +// Actual code from a test +class State { + @tracked num = 2; +} + +let state = new State(); + +setOwner(state, this.owner); + +await render(); +``` + +
+ +
Example with arguments + +Imagine you want to compute the hypotenuse of a triangle, +but all calculations are asynchronous (maybe the measurements exist on external APIs or something). + +```ts +// Actual code from a test +type NumberThunk = () => number; + +const Sqrt = resourceFactory((numFn: NumberThunk) => + trackedFunction(async () => { + let num = numFn(); + + return Math.sqrt(num); + }) +); + +const Squared = resourceFactory((numFn: NumberThunk) => + trackedFunction(async () => { + let num = numFn(); + + return Math.pow(num, 2); + }) +); + +const Hypotenuse = resourceFactory((aFn: NumberThunk, bFn: NumberThunk) => { + return resource(({ use }) => { + const aSquared = use(Squared(aFn)); + const bSquared = use(Squared(bFn)); + const c = use( + Sqrt(() => { + return (aSquared.current.value ?? 0) + (bSquared.current.value ?? 0); + }) + ); + + // We use the function return because we want this property chain + // to be what's lazily evaluated -- in this example, since + // we want to return the hypotenuse, we don't (atm) + // care about loading / error state, etc. + // In real apps, you might care about loading state though! + return () => c.current.value; + + // In situations where you care about forwarding other states, + // you could do this + return { + get value() { + return c.current.value; + }, + get isLoading() { + return ( + a.current.isLoading || b.current.isLoading || c.current.isLoading + ); + }, + }; + }); +}); +``` + +
diff --git a/test-app/tests/utils/function/rendering-test.gts b/test-app/tests/utils/function/rendering-test.gts index 23b725250..ef11d6a3a 100644 --- a/test-app/tests/utils/function/rendering-test.gts +++ b/test-app/tests/utils/function/rendering-test.gts @@ -143,9 +143,30 @@ module('Utils | trackedFunction | rendering', function (hooks) { assert.dom('out').hasText('4'); }); - test('can be composed with the resource use', async function (assert) { - type NumberThunk = () => number; + type NumberThunk = () => number; + test('can be composed directly within a resource', async function (assert) { + const Doubled = resourceFactory((num: number) => { + return resource(({ use }) => { + let state = use(trackedFunction(() => num * 2)); + + return () => state.current.value; + }); + }); + class State { + @tracked num = 2; + } + + let state = new State(); + + setOwner(state, this.owner); + + await render(); + + assert.dom('out').hasText('4'); + }); + + test('can be composed with the resource use', async function (assert) { const Sqrt = resourceFactory((numFn: NumberThunk) => trackedFunction(async () => { let num = numFn();