Description
Which @angular/* package(s) are relevant/related to the feature request?
core
Description
I have mentioned this during the RFC. I didn't want to bring it up directly after the Resource-RFC and made sure to use Resources in real applications. However, I keep running into cases where lazy-loaded resources would be more appropriate than eager-loaded (is that even a term?) resources.
Some concrete example:
In our application, a user selects an "organisation" (as in a company) When an organisation is selected, all components refer to that specific organisation.
Our application has two types of components.
- Regular Components
- Admin Components
Most of the time, regardless of whether you are admin or a regular user, you will be using the regular components.
Whenever you enter an Admin Component, we need admin-related data for the currently selected organisation. To simplify the use-case, let's say that that admin-related data comes from an endpoint that is heavy to our API and we want to avoid reloading that data unless it's absolutely required.
We only want to reload that data if:
- You are in an Admin Component AND
- You switch the current organisation to another.
We don't want to reload the data if you move from Admin Component -> Regular Component -> Admin Component without changing the organisation.
@Injectable({providedIn: "root"})
class OrgService {
public readonly orgId = signal<number>(42);
// Only needed in admin components
public readonly adminData = resource({
request: this.orgId,
loader: async ({request: orgId}) => {
return this.load(orgId);
},
});
private load(orgId: number): Promise<Org> {
// Heavy request for our API.
return Promise.resolve(new Org());
}
}
Proposed solution
Add a flag to the resource API that would make it behave like computed
rather than effect
, let's call it lazy: boolean
.
This flag would make the resource only run the fetcher if any of the state
, value
, or error
signals is currently being tracked in user-land.
If the signals are no longer tracked, the resource should still hold on to its value, in case those signals are tracked again.
If the source signal changes, the resource gets cleared and its status goes to Idle if the resource is untracked or to Loading if the resource is tracked.
This would address the address the main point that was made against my idea of making this behaviour the default behaviour, which was the this might result in potential waterfalling of resources. Since this is opt-in, you'd understand the consequence.
Alternatives considered
- No lazy loading: The first time, the resource would be loaded when needed, but then, if the user switches organisations, it will reload the resource regardless of needing it or not.
- Have the service not be
providedIn: "root"
, but only in admin components, but then it would trigger the reload whenever you leave and re-enter. - Somehow build a source-signal that will be
undefined
under the right circumstances described above, but that is error-prone since it's very hacky. - Have the resource be in a
"providedIn: null"
and cache responses statically. But this does defeat the purpose of resources and isn't a general-use solution, you'd have to find an appropriate hack for all your use cases.
@Injectable({providedIn: null})
class OrgAdminService {
private static lastCachedOrgId: number | undefined;
private static cachedAdminData: AdminData | undefined;
public readonly orgId = signal<number>(42);
// Only needed in admin components
public readonly adminData = resource({
request: this.orgId,
loader: async ({request: orgId, abortSignal}) => {
if (orgId === OrgAdminService.lastCachedOrgId) {
return OrgAdminService.cachedAdminData;
}
OrgAdminService.lastCachedOrgId = undefined;
OrgAdminService.cachedAdminData = undefined;
const loadedValue = this.loadAdminData(orgId);
if (!abortSignal.aborted) {
OrgAdminService.lastCachedOrgId = orgId;
OrgAdminService.cachedAdminData = loadedValue;
}
return loadedValue;
},
});
private loadAdminData(orgId: number): Promise<AdminData> {
// Heavy request for our API.
return Promise.resolve(new AdminData());
}
}