<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: flo-dmtx</title>
    <description>The latest articles on DEV Community by flo-dmtx (@flodmtx).</description>
    <link>https://dev.to/flodmtx</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4056483%2Ff813b847-dc58-441f-9bb9-bd4da84ce9f5.jpg</url>
      <title>DEV Community: flo-dmtx</title>
      <link>https://dev.to/flodmtx</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/flodmtx"/>
    <language>en</language>
    <item>
      <title>`resource({ loadStrategy: 'whenTracked' })`: what should wake a lazy resource?</title>
      <dc:creator>flo-dmtx</dc:creator>
      <pubDate>Wed, 05 Aug 2026 19:46:27 +0000</pubDate>
      <link>https://dev.to/flodmtx/resource-load-whentracked-what-should-wake-a-lazy-resource-2ljp</link>
      <guid>https://dev.to/flodmtx/resource-load-whentracked-what-should-wake-a-lazy-resource-2ljp</guid>
      <description>&lt;p&gt;&lt;a href="https://dev.to/flodmtx/resource-lazy-true-the-laziness-we-lost-when-we-left-the-async-pipe-5e70"&gt;A few days ago, I proposed &lt;code&gt;resource({ lazy: true })&lt;/code&gt; for Angular&lt;/a&gt;: resources load eagerly by design, and I wanted one that waits until something actually looks at it. This is the follow-up, because the design changed: for the better, and for reasons worth writing down. The whole thing turns on a question that looks trivial and is not: a lazy resource waits. Waits for &lt;em&gt;what&lt;/em&gt;, exactly?&lt;/p&gt;

&lt;h2&gt;
  
  
  Hypothesis 1: any read wakes it
&lt;/h2&gt;

&lt;p&gt;That was v1's answer. Read &lt;code&gt;value()&lt;/code&gt;, &lt;code&gt;status()&lt;/code&gt;, &lt;code&gt;hasValue()&lt;/code&gt;, from anywhere, and the loader fires. I picked that rule for two reasons, and they are solid. No template guard can deadlock, because &lt;code&gt;@if (r.hasValue())&lt;/code&gt; is itself a read. And composition works for free: &lt;code&gt;resourceFromSnapshots&lt;/code&gt;, the composition API Angular documents, reads &lt;code&gt;input.snapshot&lt;/code&gt; inside a &lt;code&gt;linkedSignal&lt;/code&gt; computation. Since that read is a read like any other, wrapping a lazy resource just works.&lt;/p&gt;

&lt;p&gt;Then a contributor, wartab, brought the best kind of pushback to the issue thread: a production implementation. Their team runs a userland lazy resource where plain reads do &lt;em&gt;not&lt;/em&gt; trigger anything: only long-term interest counts, meaning a template or an effect; &lt;code&gt;untracked&lt;/code&gt; never wakes anything; imperative code gets an explicit &lt;code&gt;loadValue()&lt;/code&gt; that returns a promise. It works, in production, today.&lt;/p&gt;

&lt;p&gt;So: two opposite rules, each backed by something real. Mine by composition, theirs by usage. When two contradictory answers both hold up under pressure, the question is usually cut along the wrong axis. Finding the right axis took two more iterations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hypothesis 2: give every intent its own method
&lt;/h2&gt;

&lt;p&gt;The disagreement seemed to be about intent. Some reads mean "I need this data, go get it". Other reads just want to look at the current state without causing anything. v1 collapsed both into one rule; wartab's model collapsed them into another. So the next hypothesis: stop collapsing. If reads carry different intents, give each intent its own entry point.&lt;/p&gt;

&lt;p&gt;That design looks like this: &lt;code&gt;peek()&lt;/code&gt; reads the state and triggers nothing. &lt;code&gt;toPromise()&lt;/code&gt; (wartab's &lt;code&gt;loadValue()&lt;/code&gt;, which I adopted under a new name) asks for the value imperatively and waits for it. An option picks the trigger strategy, including one where the resource goes back to sleep when nobody watches it. Every intent gets a door. On paper, nothing is missing.&lt;/p&gt;

&lt;p&gt;Building it is what broke it. The sleep-capable strategy kept producing states I could not defend. Wake the resource with a single read from an event handler: nobody is listening, so "the last watcher leaves" can never happen, so it never goes back to sleep. Awake forever, for a caller that got &lt;code&gt;undefined&lt;/code&gt; back and will never hear anything more. &lt;code&gt;toPromise()&lt;/code&gt; on that same strategy was almost comical: the promise resolves, and the value it just delivered is immediately thrown away, because nobody is still listening. And &lt;code&gt;peek()&lt;/code&gt; needed guard after guard to avoid leaking internal states. Every method was an answer to a question that the model itself kept generating. When every edge case needs its own branch, the state model is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rx detour
&lt;/h2&gt;

&lt;p&gt;The tension (what should wake a lazy resource) had been there from the very start. What I was missing was a way to state it in terms that already have answers. So I translated the design into RxJS, a model with a decade of production wear on exactly these questions.&lt;/p&gt;

&lt;p&gt;The mapping is short. A resource is a &lt;strong&gt;hot observable&lt;/strong&gt;: it runs whether or not anyone is watching. What my lazy option was trying to revive is the &lt;strong&gt;cold observable subscribed by the &lt;code&gt;async&lt;/code&gt; pipe&lt;/strong&gt;, the exact pattern from the first article: nothing runs until the template subscribes, and the subscription is what starts the work. Complete the equivalence with &lt;code&gt;shareReplay&lt;/code&gt; (many readers, one execution, the latest value replayed to late arrivals) and it holds: a lazy resource is a cold, shared observable, and listening to it is subscribing to it.&lt;/p&gt;

&lt;p&gt;Stated in those terms, two things became visible that I had not managed to see before.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;&lt;code&gt;refCount&lt;/code&gt;&lt;/strong&gt;. A shared observable can reset when its subscriber count drops back to zero. Modern Rx spells it &lt;code&gt;share({resetOnRefCountZero: true})&lt;/code&gt;. That is the moment wartab's model clicked into place for me. The feature their implementation was actually built around is &lt;em&gt;the resource can return to &lt;code&gt;idle&lt;/code&gt;&lt;/em&gt;, and that is &lt;code&gt;refCount&lt;/code&gt; behavior, a named, well-understood thing. &lt;code&gt;loadValue()&lt;/code&gt; was never the need; it is the consequence of an implementation choice. Once I could attach "going back to idle" to use cases I understood, the conversation stopped being about which reads should trigger and started being about lifecycle.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;subscriptions have callbacks, and a promise is not a stream&lt;/strong&gt;. Waking a lazy resource is subscribing to it. A read inside a reactive context can genuinely subscribe: it leaves a listener behind, someone the load will actually serve. A read outside a reactive context cannot: it gets a value once, and there is no channel for anything later. Waking the resource for it serves nobody: the caller got &lt;code&gt;undefined&lt;/code&gt; and will never be notified. That one distinction resolves the original tension: reads that can subscribe wake the resource; reads that cannot, photograph it. It also kills &lt;code&gt;toPromise()&lt;/code&gt; on principle: wanting exactly one value out of a stream is the wrong primitive. The rare genuine need already composes from existing bridges: &lt;code&gt;firstValueFrom(toObservable(...))&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why would a resource go back to idle?
&lt;/h2&gt;

&lt;p&gt;One thing left to formalize: what is returning-to-idle &lt;em&gt;for&lt;/em&gt;? To make a new request? Not quite: &lt;code&gt;reload()&lt;/code&gt; already does that. To free the value when nothing is using it? Closer. Combine the two: drop the value when nobody needs it anymore, &lt;strong&gt;and&lt;/strong&gt; fetch fresh data when someone comes back. That is the use case: an automatic refresh on return, with no stale data kept alive in between. Think of a details panel: close it, its data is disposed; reopen it a minute later, it loads current data, not last minute's.&lt;/p&gt;

&lt;p&gt;What about refreshing &lt;em&gt;while&lt;/em&gt; the resource is being watched? That is &lt;code&gt;reload()&lt;/code&gt;, and the split holds for a precise reason: there is exactly one situation where no code has a handle to call &lt;code&gt;reload()&lt;/code&gt;: the moment the last watcher leaves. Every other refresh scenario has an event or a code path attached to it, some place where calling &lt;code&gt;reload()&lt;/code&gt; is natural. So the last-watcher transition is the single point where the resource must act on its own; everywhere else stays explicit.&lt;/p&gt;

&lt;p&gt;From there, the model stops needing methods and collapses into two cases with clean semantics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;loadStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;whenTracked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loader&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;   &lt;span class="c1"&gt;// first listener starts the load; value kept afterwards&lt;/span&gt;
&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;loadStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;whileTracked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loader&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;  &lt;span class="c1"&gt;// lives exactly while listened to&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One invariant behind both: &lt;strong&gt;no load ever runs while nothing tracks the resource.&lt;/strong&gt; &lt;code&gt;when&lt;/code&gt; is a trigger, &lt;code&gt;while&lt;/code&gt; is a lifetime, and the pair carries the whole contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  What fell out
&lt;/h2&gt;

&lt;p&gt;The API ended smaller than where v1 started. &lt;code&gt;peek()&lt;/code&gt; is gone: a read outside a reactive context already is one. &lt;code&gt;toPromise()&lt;/code&gt; is gone too, and it is the telling one: the method both designs kept while disagreeing on everything else. With the right trigger found, it had nothing left to do.&lt;/p&gt;

&lt;p&gt;The disagreement dissolved the same way. Liveness propagates transitively through &lt;code&gt;computed&lt;/code&gt;s, so listening to a wrapper wakes the source through every layer. wartab's rule and my composition constraint were never in conflict. They were sitting on different axes.&lt;/p&gt;

&lt;p&gt;The price in core is small. Two optional hooks on the reactive graph, 22 lines, fired on the live-consumer transitions the graph already maintains (they happen to be the &lt;code&gt;watched&lt;/code&gt; / &lt;code&gt;unwatched&lt;/code&gt; callbacks of the TC39 Signals proposal). About 190 lines in &lt;code&gt;resource.ts&lt;/code&gt;. The public API diff is exactly one option. The lazy behaviors are a 43-spec contract run against both strategies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes
&lt;/h2&gt;

&lt;p&gt;The issue is open, an Angular team member left it open for community discussion. To support this feature or take part in the discussion, head to &lt;a href="https://github.com/angular/angular/issues/70036" rel="noopener noreferrer"&gt;the issue&lt;/a&gt;. The &lt;a href="https://flo-dmtx.github.io/lazy-resource-playground/" rel="noopener noreferrer"&gt;playground&lt;/a&gt; runs the actual branch diff if you want to watch &lt;code&gt;whileTracked&lt;/code&gt; cancel a request in the network log, and the &lt;a href="https://gist.github.com/flo-dmtx/e8c9ff69bec58adf85e902eab9f7d900" rel="noopener noreferrer"&gt;gist&lt;/a&gt; is the copy-paste version for today.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://github.com/wartab" rel="noopener noreferrer"&gt;wartab&lt;/a&gt; for the pushback and the discussion.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>resource({ lazy: true }): the laziness we lost when we left the async pipe</title>
      <dc:creator>flo-dmtx</dc:creator>
      <pubDate>Fri, 31 Jul 2026 12:22:57 +0000</pubDate>
      <link>https://dev.to/flodmtx/resource-lazy-true-the-laziness-we-lost-when-we-left-the-async-pipe-5e70</link>
      <guid>https://dev.to/flodmtx/resource-lazy-true-the-laziness-we-lost-when-we-left-the-async-pipe-5e70</guid>
      <description>&lt;p&gt;An observable does nothing until something subscribes. For years that gave Angular data loading a property we barely had to think about: expose a cold observable from a service, drop an &lt;code&gt;async&lt;/code&gt; pipe in the template, and the request fired only when the view actually displayed it. A panel behind an &lt;code&gt;@if&lt;/code&gt; that stayed false never subscribed, so it never fetched.&lt;/p&gt;

&lt;p&gt;Then signals arrived, and &lt;code&gt;resource()&lt;/code&gt; and &lt;code&gt;rxResource()&lt;/code&gt; are a real upgrade: statuses, errors, cancellation, a value you can read anywhere. I have no desire to go back. But resources are eager by design: they start their loader from an effect at construction, so declaring a resource is fetching it. Somewhere in that upgrade, the laziness quietly disappeared.&lt;/p&gt;

&lt;p&gt;What the framework nudges you toward instead is this component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// this component exists to defer one fetch&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserCard&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rxResource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!--
  created: fetches.
  destroyed: forgets.
  shown again: fetches again.
--&amp;gt;&lt;/span&gt;
@if (showCard()) {
    &lt;span class="nt"&gt;&amp;lt;user-card&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The component has no real job. It exists so that its lifecycle can play the part of the missing laziness: the only way to say "don't fetch this yet" is to push the resource down into a component that does not exist yet, behind an &lt;code&gt;@if&lt;/code&gt; or inside a &lt;code&gt;@defer&lt;/code&gt; block. The component lifecycle becomes your fetch trigger.&lt;/p&gt;

&lt;p&gt;It works. That is exactly why it spreads. And every time it spreads, you pay three costs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The value dies with the component.&lt;/strong&gt; Close the panel, open it again: refetch. The user pays for your architecture with a spinner, every single time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data fetching scatters down the component tree.&lt;/strong&gt; The route or service that owns the screen no longer owns its data. To find out what a page loads, you walk the tree and collect
resources from leaf components.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The codebase accumulates wrapper components.&lt;/strong&gt; Each one is a file, a selector, a test, a name in your head. Their only feature is delaying one request.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;@defer&lt;/code&gt; does not save you here. It defers a block of the view and its code chunk, which is great, but it says nothing about data. Nothing on the resources side defers the data itself. So we all keep reaching for the one deferral mechanism we have, the component lifecycle, and we keep paying those three costs.&lt;/p&gt;

&lt;p&gt;For a long time I simply refused the pattern: I would rather fetch everything eagerly and pay for a few requests nobody looks at than scatter data fetching down the tree. The day I caught myself writing &lt;code&gt;computed()&lt;/code&gt; wrappers whose only purpose was to create a resource on its first read, I stopped treating this as an architecture trade-off and started treating it as a missing primitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is one line
&lt;/h2&gt;

&lt;p&gt;Here is the proposal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                  &lt;span class="c1"&gt;// the only new line&lt;/span&gt;
    &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// rxResource({ lazy: true, ... }) — inherited through its options, no other change&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;lazy: true&lt;/code&gt;, the load effect is simply not created. Instead, the &lt;strong&gt;first read of any of the resource's signals&lt;/strong&gt; starts the load. Templates read signals, so what renders is what fetches. The &lt;code&gt;UserCard&lt;/code&gt; above becomes: declare the resource in the parent (or the route, or a service), read it wherever it shows. No wrapper.&lt;/p&gt;

&lt;p&gt;Notice how each of the three costs falls away, and not by accident:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The resource lives where you declared it, so its value outlives the panel that reads it. First visit fetches; revisit shows the kept value. No refetch, no spinner.&lt;/li&gt;
&lt;li&gt;The declaration moves &lt;em&gt;up&lt;/em&gt; the tree, back to the route or service that owns the screen. The trigger stays down in the view, where it belongs, as a plain signal read.&lt;/li&gt;
&lt;li&gt;The wrapper components can be deleted. That is my favorite kind of feature: one that removes code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The semantics: the decisions behind "the first read fetches"
&lt;/h2&gt;

&lt;p&gt;A one-line option still has to behave correctly in every corner, and I had to make some calls. These are the ones worth knowing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every read wakes it, even inside &lt;code&gt;untracked()&lt;/code&gt;.&lt;/strong&gt; Suppose only untracked reads could stay silent, and your template does &lt;code&gt;@if (r.hasValue())&lt;/code&gt;. The read returns false, nothing reactive changed, so no change detection cycle ever comes back to ask again. The template asked the question once, got "not yet", and the resource took "not yet" as permission to sleep forever: the guard that was supposed to reveal the data is the thing keeping it hidden. So: any read wakes the resource. A non-waking &lt;code&gt;peek()&lt;/code&gt; is a natural follow-up if you truly need to look without touching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Params changes while asleep are tracked but never fetched.&lt;/strong&gt; The first read fetches once, with the latest params value. A user can change every filter in a form before opening the results panel and the network stays idle. Then one read, one request. I like this one because it falls out of thinking of &lt;code&gt;params&lt;/code&gt; as &lt;em&gt;state&lt;/em&gt;, not as &lt;em&gt;events&lt;/em&gt;. The loader does not care how the params got to their current value, only what they are when someone finally wants the data. Once you frame it that way there is nothing to queue and nothing to replay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Writes never wake it.&lt;/strong&gt; &lt;code&gt;set()&lt;/code&gt; on a never-read resource goes to the &lt;code&gt;local&lt;/code&gt; status without running the loader. &lt;code&gt;reload()&lt;/code&gt; defers the load to the next read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Laziness composes.&lt;/strong&gt; A &lt;code&gt;computed()&lt;/code&gt; over a lazy resource is itself lazy. Chained resources (one's params reading another's value) stay asleep end to end, and one read at the end of the chain wakes them in order, never in parallel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Once awake, it is exactly an eager resource.&lt;/strong&gt; Same statuses, same errors, same cancellation, same SSR stability. The only documented caveat: combining &lt;code&gt;lazy&lt;/code&gt; with &lt;code&gt;id&lt;/code&gt; (SSR transfer) is discouraged, because the hydration window has usually closed by the first read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wasn't this already rejected?
&lt;/h2&gt;

&lt;p&gt;Yes, mostly. Issue &lt;a href="https://github.com/angular/angular/issues/58422" rel="noopener noreferrer"&gt;#58422&lt;/a&gt; asked for a &lt;code&gt;lazy&lt;/code&gt; option and the Angular team closed it. Their concern was legitimate: render-driven data fetching invites request waterfalls, and data is generally better lifted up to routes.&lt;/p&gt;

&lt;p&gt;I agree with the concern. But look at where that closure left us. The workaround everyone uses instead, fetching tied to a component lifecycle, &lt;em&gt;is&lt;/em&gt; render-driven fetching in its most fragile form, and it re-runs the waterfall on every component recreation. Each time the panel reopens, the whole chain of requests fires again. So the honest summary of the status quo is: Angular says "lift your data up", and then the only supported way to defer a fetch is to push it down.&lt;/p&gt;

&lt;p&gt;Compare that with the wrapper component on the three points of the concern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Where is the declaration?&lt;/strong&gt; Up the tree, at the route or service. The wrapper pushed it down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How many times does the trigger fire?&lt;/strong&gt; Once, on first read. The wrapper refires on every recreation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who owns the value?&lt;/strong&gt; The service. It survives its readers. The wrapper's value died with the DOM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The waterfall has nowhere to repeat. And it is strictly opt-in: eager resources are untouched. It also composes with &lt;code&gt;@defer&lt;/code&gt; instead of competing with it: the deferred view brings the code, the lazy resource in the parent brings the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The proof, in numbers
&lt;/h2&gt;

&lt;p&gt;I did not want this to be an opinion. Building custom signal primitives is how I usually attack this kind of gap, so I built the proposal three times, and scored the userland attempts against a parameterized suite of &lt;strong&gt;39 observable behaviors&lt;/strong&gt;: the sleeping rules above, plus everything a native resource must keep doing once awake.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;strategy&lt;/th&gt;
&lt;th&gt;lines&lt;/th&gt;
&lt;th&gt;contract&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gate the params on a "shown" flag (the common workaround)&lt;/td&gt;
&lt;td&gt;~60&lt;/td&gt;
&lt;td&gt;18/39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;drive core's private &lt;code&gt;loadEffect&lt;/code&gt; (2 private fields, do not ship)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;34/39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reimplement beside core, public API only&lt;/td&gt;
&lt;td&gt;~370&lt;/td&gt;
&lt;td&gt;39/39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the native option (this proposal)&lt;/td&gt;
&lt;td&gt;~90 in ResourceImpl&lt;/td&gt;
&lt;td&gt;by construction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few things this table taught me. The "shown flag" gate looks like a cheap fix, but it only defers the first load and misses most of the sleeping rules. Reaching into core's privates gets you to 34/39 and a dependency on two private fields; its real value was as evidence, because the behaviors it passes are exactly the ones you get for free when the laziness lives where the effect is created. And the full reimplementation does pass everything, but it costs ~370 lines to reproduce what the native option does in ~90 inside &lt;code&gt;ResourceImpl&lt;/code&gt;, because natively you are not fighting the design, you are just not creating one effect.&lt;/p&gt;

&lt;p&gt;The native version is real code, not a sketch: branch &lt;a href="https://github.com/flo-dmtx/angular/tree/feat/lazy-resource" rel="noopener noreferrer"&gt;&lt;code&gt;feat/lazy-resource&lt;/code&gt;&lt;/a&gt; on my fork of angular/angular. One commit, a 500-line spec, docs. When &lt;code&gt;lazy&lt;/code&gt; is set the load effect is not created; a small reactive pull node takes its place, pulled by &lt;code&gt;value&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt;. On that branch, &lt;code&gt;//packages/core/test/resource&lt;/code&gt; passes &lt;strong&gt;91/91&lt;/strong&gt; (the new lazy suite plus every pre-existing spec, unchanged), &lt;code&gt;rxjs-interop&lt;/code&gt; passes &lt;strong&gt;76/76&lt;/strong&gt;, and the public API goldens are green, all with the repo's own Bazel tooling. &lt;code&gt;rxResource&lt;/code&gt; inherits the option through its existing options spread: zero interop code change.&lt;/p&gt;

&lt;p&gt;If you would rather see it than believe a table, the &lt;a href="https://flo-dmtx.github.io/lazy-resource-playground/" rel="noopener noreferrer"&gt;interactive proposal page&lt;/a&gt; pins &lt;code&gt;@angular/core&lt;/code&gt; 22.1.0 and applies the native diff to the published bundle with patch-package, so the option exists in your browser even though it does not exist in Angular. Every demo on the page runs the actual &lt;code&gt;rxResource({ lazy: true })&lt;/code&gt;, with a per-demo network log showing every request that leaves and, more importantly, every request that does not. You can watch a parent-owned resource being read by a child, tabs that fetch on first visit and keep their value on revisit, params changing while the resource sleeps, a chain waking in order, and the full contract with &lt;code&gt;reload&lt;/code&gt;, &lt;code&gt;set&lt;/code&gt;, a 503 and the recovery. The same project runs on &lt;a href="https://stackblitz.com/~/github.com/flo-dmtx/lazy-resource-playground" rel="noopener noreferrer"&gt;StackBlitz&lt;/a&gt; if you want to poke at the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use it today, and help it land
&lt;/h2&gt;

&lt;p&gt;Two things you can do, one for you and one for the feature.&lt;/p&gt;

&lt;p&gt;For you: the 39/39 userland implementation is a single file with no private imports, published as &lt;a href="https://gist.github.com/flo-dmtx/e8c9ff69bec58adf85e902eab9f7d900" rel="noopener noreferrer"&gt;a gist&lt;/a&gt;. It exports &lt;code&gt;lazyResource&lt;/code&gt; (promise loader) and &lt;code&gt;lazyRxResource&lt;/code&gt; (observable stream). Copy the file into your project, swap &lt;code&gt;rxResource&lt;/code&gt; for &lt;code&gt;lazyRxResource&lt;/code&gt;, and start deleting wrapper components this week.&lt;/p&gt;

&lt;p&gt;For the feature: I have opened a feature request for the native option at &lt;a href="https://github.com/angular/angular/issues/70036" rel="noopener noreferrer"&gt;angular/angular#70036&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have written the &lt;code&gt;UserCard&lt;/code&gt; component above, in any of its forms, a thumbs-up helps; a comment describing your own wrapper-component pile helps more, because "who actually needs this" is the question the team will ask. The previous issue was closed on a reasonable concern; what it was missing, I suspect, is evidence of how many of us are living with the workaround. That part is on us.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>signal</category>
    </item>
  </channel>
</rss>
