{"id":51377192,"url":"https://github.com/zxnc/zx-angular-lazy-resource","last_synced_at":"2026-07-05T21:00:32.172Z","repository":{"id":367027487,"uuid":"1278864506","full_name":"zxnc/zx-angular-lazy-resource","owner":"zxnc","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-24T09:55:17.000Z","size":32,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-24T10:35:25.327Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zxnc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-24T06:56:45.000Z","updated_at":"2026-06-24T09:55:21.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/zxnc/zx-angular-lazy-resource","commit_stats":null,"previous_names":["zxnc/zx-angular-lazy-resource"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/zxnc/zx-angular-lazy-resource","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zxnc%2Fzx-angular-lazy-resource","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zxnc%2Fzx-angular-lazy-resource/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zxnc%2Fzx-angular-lazy-resource/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zxnc%2Fzx-angular-lazy-resource/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zxnc","download_url":"https://codeload.github.com/zxnc/zx-angular-lazy-resource/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zxnc%2Fzx-angular-lazy-resource/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35168795,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-05T02:00:06.290Z","response_time":100,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-03T14:00:27.940Z","updated_at":"2026-07-05T21:00:32.166Z","avatar_url":"https://github.com/zxnc.png","language":"TypeScript","funding_links":[],"categories":["Architecture and Advanced Topics"],"sub_categories":["HTTP"],"readme":"# zx-angular-lazy-resource\n\n\u003e Lazy helpers for Angular's signal-based `resource()` — defer loading until first access, and `await` the first settled value as a Promise.\n\n[![npm version](https://img.shields.io/npm/v/zx-angular-lazy-resource.svg)](https://www.npmjs.com/package/zx-angular-lazy-resource)\n[![CI](https://github.com/zxnc/zx-angular-lazy-resource/actions/workflows/ci.yml/badge.svg)](https://github.com/zxnc/zx-angular-lazy-resource/actions/workflows/ci.yml)\n[![license](https://img.shields.io/npm/l/zx-angular-lazy-resource.svg)](./LICENSE)\n\nTwo tiny, dependency-light utilities built on top of Angular's `resource()`:\n\n- **`lazyResource()`** — a `resource()` that does **not** fire its request on app\n  startup. The loader runs only the **first time the resource is accessed**.\n- **`takeLazyResource()`** — `await` a resource and get its **first real value**\n  (never the empty/default one), as a `Promise`.\n\n---\n\n## The problem\n\nAngular's `resource()` is great, but its loader runs **as soon as the resource is\ncreated**. If you keep a bunch of shared resources in a service…\n\n```ts\n@Service()\nexport class GlobalStore {\n  private api = inject(Api);\n\n  // ❌ Every one of these fires an HTTP request the moment the app boots,\n  //    even if the user never opens the screen that needs them.\n  readonly brands     = resource({ loader: () =\u003e this.api.getBrands(),     defaultValue: [] });\n  readonly currencies = resource({ loader: () =\u003e this.api.getCurrencies(), defaultValue: [] });\n  readonly vats       = resource({ loader: () =\u003e this.api.getVats(),       defaultValue: [] });\n}\n```\n\n…you end up flooding your backend with requests at startup for data you may not\nneed yet.\n\n`lazyResource()` fixes this: each resource waits until it is actually read.\n\n```ts\n@Service()\nexport class GlobalStore {\n  private api = inject(Api);\n\n  // ✅ No request until something reads `.value()` (template, computed, await, ...).\n  readonly brands     = lazyResource(() =\u003e this.api.getBrands(),     []);\n  readonly currencies = lazyResource(() =\u003e this.api.getCurrencies(), []);\n  readonly vats       = lazyResource(() =\u003e this.api.getVats(),       []);\n}\n```\n\n---\n\n## Installation\n\n```bash\nnpm install zx-angular-lazy-resource\n```\n\n**Peer dependencies:** `@angular/core` (\u003e= 22) and `rxjs` (\u003e= 7) — both already\npresent in any Angular app.\n\n---\n\n## Quick start\n\n### 1. Declare lazy resources\n\n```ts\nimport { Service, inject } from '@angular/core';\nimport { lazyResource } from 'zx-angular-lazy-resource';\n\n@Service()\nexport class CatalogStore {\n  private api = inject(CatalogApi);\n\n  readonly brands = lazyResource(() =\u003e this.api.getBrands(), []);\n}\n```\n\n### 2a. Use it synchronously (template / computed)\n\nReading `.value()` works exactly like a normal resource — and it transparently\ntriggers the load the first time:\n\n```ts\n@Component({\n  template: `\n    @if (store.brands.isLoading()) {\n      \u003cspan\u003eLoading…\u003c/span\u003e\n    } @else {\n      @for (brand of store.brands.value(); track brand.id) {\n        \u003cdiv\u003e{{ brand.name }}\u003c/div\u003e\n      }\n    }\n  `,\n})\nexport class BrandsComponent {\n  protected store = inject(CatalogStore);\n}\n```\n\n### 2b. Use it asynchronously (`await` the first real value)\n\n`value()` is synchronous, so on the very first read it may still be the default\n(`[]`). When you need to be sure you have the server response, `await` it:\n\n```ts\nasync function loadActiveBrands(store: CatalogStore) {\n  const brands = await takeLazyResource(store.brands);\n  return brands.filter((b) =\u003e b.active); // never runs on an empty default\n}\n```\n\nA convenient pattern is to expose an `...Async` helper next to each resource:\n\n```ts\n@Service()\nexport class CatalogStore {\n  private api = inject(CatalogApi);\n\n  readonly brands = lazyResource(() =\u003e this.api.getBrands(), []);\n  \n  readonly brandsAsync = () =\u003e takeLazyResource(this.brands);\n}\n\n// somewhere else:\nconst brands = await store.brandsAsync();\n```\n\n---\n\n## API\n\n### `lazyResource\u003cT\u003e(loader, defaultValue, optionsOrInjector?)`\n\n| Param               | Type                              | Description                                                                                |\n| ------------------- | --------------------------------- | ------------------------------------------------------------------------------------------ |\n| `loader`            | `() =\u003e Promise\u003cT\u003e`                | Async function that fetches the data. Runs once, on first access.                          |\n| `defaultValue`      | `T`                               | Value exposed by `value()` before the loader resolves.                                     |\n| `optionsOrInjector` | `LazyResourceOptions \\| Injector` | Optional. An options object (see below) or, for backwards compatibility, a bare `Injector`. |\n\n`LazyResourceOptions`:\n\n| Property   | Type                  | Description                                                                                       |\n| ---------- | --------------------- | ------------------------------------------------------------------------------------------------- |\n| `id`       | `string` (optional)   | Enables Angular's **SSR `TransferState` caching** for this resource (see below).                  |\n| `injector` | `Injector` (optional) | Only needed when called **outside** an injection context. Defaults to `inject(Injector)`.         |\n\n**Returns** `ResourceRef\u003cT\u003e` — a normal resource ref (`value()`, `status()`,\n`isLoading()`, `hasValue()`, `error()`, `reload()`, …). The only difference is\nthat the loader is deferred until the first property access.\n\n#### SSR caching with `TransferState`\n\nWhen an app renders on the server, the resource loader runs once to produce the\ninitial HTML; during hydration the browser would normally run the same loader\nagain. Provide an `id` to reuse the server result: Angular stores the resolved\nvalue in `TransferState` on the server and uses it on the client to initialize\nthe resource in a `'resolved'` state.\n\n```ts\n@Service()\nexport class UserStore {\n  private api = inject(UserApi);\n\n  // The value resolved on the server is reused on the client — no second request.\n  readonly user = lazyResource(() =\u003e this.api.getUser(), null, { id: 'current-user' });\n}\n```\n\nThe `id` must be **unique within your application** and **identical on the\nserver and the client** so Angular can match the cached entry.\n\n\u003e ⚠️ Because the cached value is serialized into the page's HTML, avoid using an\n\u003e `id` for resources that load **user-specific** data when the rendered HTML can\n\u003e be cached or shared between users.\n\nThe third argument is still backwards compatible with a bare `Injector`:\n\n```ts\nlazyResource(() =\u003e api.getBrands(), [], injector);          // injector only\nlazyResource(() =\u003e api.getBrands(), [], { injector });      // injector via options\nlazyResource(() =\u003e api.getBrands(), [], { id, injector });  // id + injector\n```\n\n### `takeLazyResource\u003cT\u003e(ref, optionsOrInjector?)`\n\n| Param               | Type                                  | Description                                                                                  |\n| ------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------- |\n| `ref`               | `ResourceRef\u003cT\u003e`                      | The resource to await. Works with `lazyResource` and with a plain `resource()`.              |\n| `optionsOrInjector` | `TakeLazyResourceOptions \\| Injector` | Optional. An options object (see below) or, for backwards compatibility, a bare `Injector`.  |\n\n`TakeLazyResourceOptions`:\n\n| Property   | Type                  | Description                                                                                  |\n| ---------- | --------------------- | -------------------------------------------------------------------------------------------- |\n| `reload`   | `boolean` (optional)  | When `true`, force a fresh fetch (`ref.reload()`) and resolve with the reloaded value.       |\n| `injector` | `Injector` (optional) | Reuses the injector captured by `lazyResource`; otherwise falls back to `inject(Injector)`.  |\n\n**Returns** `Promise\u003cT\u003e` that:\n\n- **resolves** with the first `'resolved'` (or `'local'`) value, and\n- **rejects** with the resource's error if the loader fails.\n\n#### Fresh data on every call\n\nBy default `takeLazyResource` resolves with the resource's cached value once it\nhas loaded. Pass `{ reload: true }` to force a refetch instead: it calls\n`ref.reload()`, skips the stale cached value, and resolves with the freshly\nloaded one.\n\n```ts\n// Reuses the cached value (loads once, then cached):\nconst brands = await takeLazyResource(store.brands);\n\n// Always refetches and resolves with up-to-date data:\nconst fresh = await takeLazyResource(store.brands, { reload: true });\n```\n\nIf a reload turns out to be unnecessary or unsupported (e.g. a load is already\nin flight), the first settled value is used instead, so the call never hangs.\n\n\u003e 💡 `takeLazyResource` can be called from anywhere — event handlers, `async`\n\u003e methods, etc. — because `lazyResource` captures the injection context for you.\n\u003e For a plain (non-lazy) `resource()` called outside an injection context, pass\n\u003e the `injector` (via `{ injector }`) explicitly.\n\n---\n\n## How it works\n\n`resource()` documents that **if the `params` computation returns `undefined`,\nthe loader does not run and the resource stays `'idle'`**.\n\n`lazyResource` gates `params` behind an `enabled` signal:\n\n```ts\nresource({\n  params: () =\u003e (enabled() ? true : undefined), // undefined =\u003e loader never runs\n  loader,\n  defaultValue,\n});\n```\n\nThe returned resource is wrapped in a `Proxy`. The first time any property is\nread, the proxy flips `enabled` to `true` (inside `untracked`, so it stays\nside-effect-safe), which makes `params` produce a value and the loader runs\nexactly once. Because the wrapper is a transparent `Proxy`, existing call sites\n(`.value()`, `.status()`, …) keep working unchanged.\n\n`takeLazyResource` listens to the resource's `status` signal (via\n`toObservable`) and resolves the promise when it first settles. With\n`{ reload: true }` it first calls `ref.reload()`, then waits for the resource to\nenter a `'loading'`/`'reloading'` state and resolve again — skipping the stale\ncached value so you get fresh data.\n\n---\n\n## Notes \u0026 caveats\n\n- **One request, then cached.** Once loaded, the value is cached by the\n  resource. Call `ref.reload()` to fetch again.\n- **`takeLazyResource` resolves on the first settled state.** If a `reload()` is\n  in progress, `value()` may still hold the previous value (Angular's normal\n  `'reloading'` behaviour). Use `{ reload: true }` to force a fresh fetch and\n  resolve with the reloaded value.\n- **SSR:** loading is access-driven; if you render on the server, accessing the\n  resource during rendering triggers the load there too. Pass an `id` to cache\n  the server-resolved value via `TransferState` and skip the loader on the\n  client during hydration.\n- **Reading is what triggers loading.** A resource that nothing ever reads will\n  never fire its request — which is exactly the point.\n\n---\n\n## Compatibility\n\n| Package        | Version  |\n| -------------- | -------- |\n| `@angular/core`| \u003e= 22    |\n| `rxjs`         | \u003e= 7     |\n\n`resource()` is stable as of Angular 22 (available as experimental in earlier\nversions). This package targets the stable API, so Angular 22 is the minimum\nsupported version.\n\n---\n\n## Contributing\n\nIssues and PRs are welcome. To build locally:\n\n```bash\nnpm install\nnpm run build   # outputs to ./dist\n```\n\n---\n\n## License\n\n[MIT](./LICENSE) © zxnc\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzxnc%2Fzx-angular-lazy-resource","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzxnc%2Fzx-angular-lazy-resource","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzxnc%2Fzx-angular-lazy-resource/lists"}