npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

zx-angular-lazy-resource

v1.0.4

Published

Lazy helpers for Angular's signal-based resource(): defer loading until first access, and await the first settled value as a Promise.

Readme

zx-angular-lazy-resource

Lazy helpers for Angular's signal-based resource() — defer loading until first access, and await the first settled value as a Promise.

npm version CI license

Two tiny, dependency-light utilities built on top of Angular's resource():

  • lazyResource() — a resource() that does not fire its request on app startup. The loader runs only the first time the resource is accessed.
  • takeLazyResource()await a resource and get its first real value (never the empty/default one), as a Promise.

The problem

Angular's resource() is great, but its loader runs as soon as the resource is created. If you keep a bunch of shared resources in a service…

@Service()
export class GlobalStore {
  private api = inject(Api);

  // ❌ Every one of these fires an HTTP request the moment the app boots,
  //    even if the user never opens the screen that needs them.
  readonly brands     = resource({ loader: () => this.api.getBrands(),     defaultValue: [] });
  readonly currencies = resource({ loader: () => this.api.getCurrencies(), defaultValue: [] });
  readonly vats       = resource({ loader: () => this.api.getVats(),       defaultValue: [] });
}

…you end up flooding your backend with requests at startup for data you may not need yet.

lazyResource() fixes this: each resource waits until it is actually read.

@Service()
export class GlobalStore {
  private api = inject(Api);

  // ✅ No request until something reads `.value()` (template, computed, await, ...).
  readonly brands     = lazyResource(() => this.api.getBrands(),     []);
  readonly currencies = lazyResource(() => this.api.getCurrencies(), []);
  readonly vats       = lazyResource(() => this.api.getVats(),       []);
}

Installation

npm install zx-angular-lazy-resource

Peer dependencies: @angular/core (>= 22) and rxjs (>= 7) — both already present in any Angular app.


Quick start

1. Declare lazy resources

import { Service, inject } from '@angular/core';
import { lazyResource } from 'zx-angular-lazy-resource';

@Service()
export class CatalogStore {
  private api = inject(CatalogApi);

  readonly brands = lazyResource(() => this.api.getBrands(), []);
}

2a. Use it synchronously (template / computed)

Reading .value() works exactly like a normal resource — and it transparently triggers the load the first time:

@Component({
  template: `
    @if (store.brands.isLoading()) {
      <span>Loading…</span>
    } @else {
      @for (brand of store.brands.value(); track brand.id) {
        <div>{{ brand.name }}</div>
      }
    }
  `,
})
export class BrandsComponent {
  protected store = inject(CatalogStore);
}

2b. Use it asynchronously (await the first real value)

value() is synchronous, so on the very first read it may still be the default ([]). When you need to be sure you have the server response, await it:

async function loadActiveBrands(store: CatalogStore) {
  const brands = await takeLazyResource(store.brands);
  return brands.filter((b) => b.active); // never runs on an empty default
}

A convenient pattern is to expose an ...Async helper next to each resource:

@Service()
export class CatalogStore {
  private api = inject(CatalogApi);

  readonly brands = lazyResource(() => this.api.getBrands(), []);
  
  readonly brandsAsync = () => takeLazyResource(this.brands);
}

// somewhere else:
const brands = await store.brandsAsync();

API

lazyResource<T>(loader, defaultValue, optionsOrInjector?)

| Param | Type | Description | | ------------------- | --------------------------------- | ------------------------------------------------------------------------------------------ | | loader | () => Promise<T> | Async function that fetches the data. Runs once, on first access. | | defaultValue | T | Value exposed by value() before the loader resolves. | | optionsOrInjector | LazyResourceOptions \| Injector | Optional. An options object (see below) or, for backwards compatibility, a bare Injector. |

LazyResourceOptions:

| Property | Type | Description | | ---------- | --------------------- | ------------------------------------------------------------------------------------------------- | | id | string (optional) | Enables Angular's SSR TransferState caching for this resource (see below). | | injector | Injector (optional) | Only needed when called outside an injection context. Defaults to inject(Injector). |

Returns ResourceRef<T> — a normal resource ref (value(), status(), isLoading(), hasValue(), error(), reload(), …). The only difference is that the loader is deferred until the first property access.

SSR caching with TransferState

When an app renders on the server, the resource loader runs once to produce the initial HTML; during hydration the browser would normally run the same loader again. Provide an id to reuse the server result: Angular stores the resolved value in TransferState on the server and uses it on the client to initialize the resource in a 'resolved' state.

@Service()
export class UserStore {
  private api = inject(UserApi);

  // The value resolved on the server is reused on the client — no second request.
  readonly user = lazyResource(() => this.api.getUser(), null, { id: 'current-user' });
}

The id must be unique within your application and identical on the server and the client so Angular can match the cached entry.

⚠️ Because the cached value is serialized into the page's HTML, avoid using an id for resources that load user-specific data when the rendered HTML can be cached or shared between users.

The third argument is still backwards compatible with a bare Injector:

lazyResource(() => api.getBrands(), [], injector);          // injector only
lazyResource(() => api.getBrands(), [], { injector });      // injector via options
lazyResource(() => api.getBrands(), [], { id, injector });  // id + injector

takeLazyResource<T>(ref, optionsOrInjector?)

| Param | Type | Description | | ------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------- | | ref | ResourceRef<T> | The resource to await. Works with lazyResource and with a plain resource(). | | optionsOrInjector | TakeLazyResourceOptions \| Injector | Optional. An options object (see below) or, for backwards compatibility, a bare Injector. |

TakeLazyResourceOptions:

| Property | Type | Description | | ---------- | --------------------- | -------------------------------------------------------------------------------------------- | | reload | boolean (optional) | When true, force a fresh fetch (ref.reload()) and resolve with the reloaded value. | | injector | Injector (optional) | Reuses the injector captured by lazyResource; otherwise falls back to inject(Injector). |

Returns Promise<T> that:

  • resolves with the first 'resolved' (or 'local') value, and
  • rejects with the resource's error if the loader fails.

Fresh data on every call

By default takeLazyResource resolves with the resource's cached value once it has loaded. Pass { reload: true } to force a refetch instead: it calls ref.reload(), skips the stale cached value, and resolves with the freshly loaded one.

// Reuses the cached value (loads once, then cached):
const brands = await takeLazyResource(store.brands);

// Always refetches and resolves with up-to-date data:
const fresh = await takeLazyResource(store.brands, { reload: true });

If a reload turns out to be unnecessary or unsupported (e.g. a load is already in flight), the first settled value is used instead, so the call never hangs.

💡 takeLazyResource can be called from anywhere — event handlers, async methods, etc. — because lazyResource captures the injection context for you. For a plain (non-lazy) resource() called outside an injection context, pass the injector (via { injector }) explicitly.


How it works

resource() documents that if the params computation returns undefined, the loader does not run and the resource stays 'idle'.

lazyResource gates params behind an enabled signal:

resource({
  params: () => (enabled() ? true : undefined), // undefined => loader never runs
  loader,
  defaultValue,
});

The returned resource is wrapped in a Proxy. The first time any property is read, the proxy flips enabled to true (inside untracked, so it stays side-effect-safe), which makes params produce a value and the loader runs exactly once. Because the wrapper is a transparent Proxy, existing call sites (.value(), .status(), …) keep working unchanged.

takeLazyResource listens to the resource's status signal (via toObservable) and resolves the promise when it first settles. With { reload: true } it first calls ref.reload(), then waits for the resource to enter a 'loading'/'reloading' state and resolve again — skipping the stale cached value so you get fresh data.


Notes & caveats

  • One request, then cached. Once loaded, the value is cached by the resource. Call ref.reload() to fetch again.
  • takeLazyResource resolves on the first settled state. If a reload() is in progress, value() may still hold the previous value (Angular's normal 'reloading' behaviour). Use { reload: true } to force a fresh fetch and resolve with the reloaded value.
  • SSR: loading is access-driven; if you render on the server, accessing the resource during rendering triggers the load there too. Pass an id to cache the server-resolved value via TransferState and skip the loader on the client during hydration.
  • Reading is what triggers loading. A resource that nothing ever reads will never fire its request — which is exactly the point.

Compatibility

| Package | Version | | -------------- | -------- | | @angular/core| >= 22 | | rxjs | >= 7 |

resource() is stable as of Angular 22 (available as experimental in earlier versions). This package targets the stable API, so Angular 22 is the minimum supported version.


Contributing

Issues and PRs are welcome. To build locally:

npm install
npm run build   # outputs to ./dist

License

MIT © zxnc