ember-resources
ember-resources copied to clipboard
Setting tracked state inside a resource
Hi there! I have some code like this (simplified):
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { useResource } from 'ember-resources';
import { action } from '@ember/object';
import { trackedFunction } from 'ember-resources/util/function';
let loadUsers = () => {
return new Promise((resolve) => {
setTimeout(() => resolve([1,2,3]), 1000);
});
}
export default class UserList extends Component {
@tracked isLoading = false;
list = trackedFunction(this, async () => {
this.isLoading = true;
await loadUsers();
});
}
… and used in a template like this:
{{#if this.isLoading}}
Loading
{{else}}
{{#each this.list as |user|}}
{{user}}
{{/each}}
{{/if}}
This throws a you-cannot-update-state-that-you-already-read error:
It makes sense to me given what I understand about ember's rendering: I've read isLoading
in the if
and then the trackedFunction
is trying to update it. Apparently this style of code does not work with tasks (e.g. trackedTask
either) — it results in a similar error when using the task's isRunning
state.
While the error makes sense, it also seems confusing. I can think of a few approaches to handle this, e.g. a utility like a RemoteData
that can wrap loading state inside the resource itself, so that the isLoading
state isn't mutated in the same cycle, only after the promise resolves, e.g. (also simplified)
class State<T> {
@tracked value?: T;
@tracked isLoading: boolean;
}
export function AsyncData<T>(fn: (opts: { signal: AbortSignal }) => Promise<unknown>) {
return resource(({ on }) => {
let state = new State<T>();
state.isLoading = true;
fn({ signal: controller.signal })
.then((value: T) => {
state.value = value;
}).finally(() => {
state.isLoading = false;
});
return state;
});
}
That said, I'm wondering if this is known / if we're doing something wrong, and if there are any other possible approaches to this. Thank you! 😄