Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Built-in lazy-loading support for resources #61296

Copy link
Copy link
Closed as not planned
Closed as not planned
Copy link
@wartab

Description

@wartab
Issue body actions

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());
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Morty Proxy This is a proxified and sanitized view of the page, visit original site.