This is my Thoughts a year ago when
Alexreleased the RFC and how i was studying the ResourcePattern
All examples use the Official Joke API, and the Official wikimedia stream api.
No wrapper, no Angular HTTP client: justfetch()and signals.
resource() vs httpResource(): The One-Line Difference
httpResource() |
resource() |
|
|---|---|---|
| Import | @angular/common/http |
@angular/core |
Uses HttpClient? |
Yes, including interceptors and testing utilities | No, plain fetch under your control |
| What you give it | A URL / request object | An async loader function |
| Data sources | GET-only reactive fetching | Any Promise (fetch, IndexedDB, WebSocket, timersβ¦) |
| Continuous streams | Not supported | Supported via the stream option |
| Reactive params | Built into the URL callback | Explicit params option |
| AbortSignal | Handled automatically | Passed into loader({ abortSignal })
|
resource() is the lower-level primitive.
Setup: Joke Type + API Constants
Before jumping into the patterns, here are the shared types and constants used across all examples.
// jokes.ts
export interface Joke {
id: number;
type: string;
setup: string;
punchline: string;
}
// http-demo-apis.ts
export const JOKES_API = {
JOKE_API_10_JOKES: 'https://official-joke-api.appspot.com/random_ten',
GET_JOKES_WITH_DYNAMIC_NUMBERS: (n: number) =>
`https://official-joke-api.appspot.com/jokes/random/${n}`,
};
Full Options Reference
hint:
I will always remember what Alex told me, watch out for the Resource Type where we can extend and add more to it. Let me know what more can you do with it π€ ?
resource<T, P>({
// ββ Reactive params βββββββββββββββββββββββββββββββββββββββββββββββββ
// Signal reads here register as dependencies.
// Return undefined to keep the resource idle.
params: () => ({ count: this.count() }),
// ββ Async loader ββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Receives { params, abortSignal, previous }.
// Must return a Promise<T>.
loader: async ({ params, abortSignal, previous }) => {
const res = await fetch(`/api/jokes/${params.count}`, { signal: abortSignal });
return res.json() as Promise<T>;
},
// ββ Optional extras βββββββββββββββββββββββββββββββββββββββββββββββββ
// Returned while loading/idle: value() is never undefined when set.
defaultValue: [] as T,
// Custom equality: skip re-renders when the data didn't meaningfully change.
equal: (a, b) => a.length === b.length && a.every((j, i) => j.id === b[i].id),
// SSR TransferState key: server serialises under this key;
// the client reads from cache on hydration instead of re-fetching.
id: 'jokes-resource',
// Shown in Angular DevTools signals graph.
debugName: 'JokeResourceDemo',
// Useful when creating a resource outside an injection context.
injector: inject(Injector),
});
The stream Variant
stream replaces loader entirely, the two cannot be used together on the same resource.
resource<T, R>({
// Same params / defaultValue / equal / injector / id / debugName as above.
// ββ Streaming loader ββββββββββββββββββββββββββββββββββββββββββββββββ
// Cannot be combined with `loader`.
// Returns a Signal (or a Promise of one) shaped like { value: T } | { error: Error }.
stream: async ({ params, abortSignal }) => {
const live = signal<{ value: T } | { error: Error }>({ value: initialValue });
// Wire up a WebSocket, an SSE connection, an async generator, or any
// other continuously-updating source here, then call live.set() or
// live.update() whenever new data arrives.
return live;
},
});
Heads up: since stream hands back a plain signal, it never buffers what you write to it either. If you plan to write raw incoming values with .set() instead of merging them, see the callout in Example 6 for why that can quietly drop data.
Example 1: The Pure Form
This is resource() at its absolute minimum. No params, no abort handling, no options: just a loader that returns a Promise.
In service we do:
import { resource, Service } from '@angular/core';
import { Joke } from './jokes';
import { JOKES_API } from './http-demo-service';
@Service()
export class ResourceDemoService {
pureJokeDataResource = resource({
loader: (): Promise<Joke[]> =>
fetch(JOKES_API.JOKE_API_10_JOKES).then((res) => res.json()),
});
}
No reactive params, no signal() reads. This fires once on creation and stays resolved. The loader is just an async function that returns a Promise<T>. Any async operation qualifies: fetch, indexedDB.get, crypto.subtle.digest
In component we do:
import { Component, inject } from '@angular/core';
import { ResourceDemoService } from '../service/resource-demo.service';
@Component({
selector: 'app-jokes',
templateUrl: './jokes.html',
})
export class JokesComponent {
#service = inject(ResourceDemoService);
jokes = this.#service.pureJokeDataResource;
}
In HTML then we do:
@if (jokes.isLoading()) {
<p>Loading...</p>
} @else {
@for (joke of jokes.value(); track joke.id) {
<div>
<strong>{{ joke.setup }}</strong>
<p>{{ joke.punchline }}</p>
</div>
}
}
jokes.value() can be undefined here since no defaultValue was set. Use hasValue() or a defaultValue: [] before iterating.
Example 2: Async/Await with Signal Params
Now resource() becomes reactive. The params callback is a reactive context: read a signal there and the loader re-runs every time it changes.
In service we do:
asyncAwaitJokeDataResourceWithParameter = (jokeCount: Signal<number | undefined>) => {
return resource<Joke[], { count: number } | undefined>({
params: () => (jokeCount() !== undefined ? { count: jokeCount()! } : undefined),
defaultValue: [],
loader: async ({ params }) => {
const response = await fetch(
JOKES_API.GET_JOKES_WITH_DYNAMIC_NUMBERS(params.count)
);
return await response.json();
},
});
};
params is the reactive bridge. Angular tracks signal reads inside the params callback, not inside loader. This is intentional: the loader runs in a non-reactive async context, so if you read signals there they won't register as dependencies. Always put signal reads in params.
In component we do:
import { Component, inject, signal } from '@angular/core';
import { ResourceDemoService } from '../service/resource-demo.service';
@Component({
selector: 'app-jokes-dynamic',
templateUrl: './jokes-dynamic.html',
})
export class JokesDynamicComponent {
#service = inject(ResourceDemoService);
count = signal<number | undefined>(5);
jokes = this.#service.asyncAwaitJokeDataResourceWithParameter(this.count);
increase() {
this.count.update((n) => (n ?? 0) + 1);
}
}
When count changes, Angular re-evaluates params, detects a new value, and re-runs the loader automatically.
In HTML then we do:
<input
#input
type="number"
placeholder="Type count..."
[value]="count()"
(input)="count.set(input.valueAsNumber)"
/>
<button (click)="increase()">+ More jokes</button>
@if (jokes.isLoading()) {
<p>Fetching {{ count() }} jokes...</p>
} @else if (jokes.error()) {
<p>Error: {{ jokes.error()?.message }}</p>
} @else {
@for (joke of jokes.value() ?? []; track joke.id) {
<p><strong>{{ joke.setup }}</strong>: {{ joke.punchline }}</p>
}
}
Example 3: Conditional Loading (Skip When Undefined)
Return undefined from params when the count is not ready yet, and let defaultValue take care of the rest. This is the pattern resource() was actually built for: no manual guard inside the loader, just tell Angular there is nothing to fetch yet and it puts the resource into idle status for you, value() falls back to defaultValue automatically.
In service we do:
asyncAwaitJokeDataResourceWithParameter = (jokeCount: Signal<number | undefined>) => {
return resource<Joke[], { count: number } | undefined>({
params: () => (jokeCount() !== undefined ? { count: jokeCount()! } : undefined),
defaultValue: [],
loader: async ({ params }) => {
const response = await fetch(
JOKES_API.GET_JOKES_WITH_DYNAMIC_NUMBERS(params.count)
);
return await response.json();
},
});
};
There is an alternative if you would rather keep a guard inside the loader: check the count there and return an early value like []. The difference is the resulting status. Returning undefined from params gives you idle, guarding inside loader gives you resolved with an empty result. Pick whichever status your UI wants to react to.
// Option A: idle status, value() falls back to defaultValue
params: () => jokeCount() !== undefined ? { count: jokeCount()! } : undefined,
// Option B: resolved status, value() is [] on purpose
loader: async ({ params }) => {
if (params.count === undefined) return [];
...
},
In component we do:
@Component({
selector: 'app-jokes-conditional',
templateUrl: './jokes-conditional.html',
})
export class JokesConditionalComponent {
#service = inject(ResourceDemoService);
jokesCount = signal<number | undefined>(undefined);
jokes = this.#service.asyncAwaitJokeDataResourceWithParameter(this.jokesCount);
}
In HTML then we do:
<input
#input
type="number"
placeholder="Enter a count to load jokes"
(input)="jokesCount.set(input.valueAsNumber)"
/>
@if (jokesCount() === undefined) {
<p>Enter a number above to fetch jokes.</p>
} @else if (jokes.isLoading()) {
<p>Loading...</p>
} @else if (jokes.error()) {
<p>Something went wrong: {{ jokes.error()?.message }}</p>
} @else {
@for (joke of jokes.value(); track joke.id) {
<div>
<strong>{{ joke.setup }}</strong>
<p>{{ joke.punchline }}</p>
</div>
} @empty {
<p>No jokes found for that count.</p>
}
}
Example 4: AbortSignal for Cancellation Support
This is where resource() pulls ahead of higher-level wrappers. The loader receives an abortSignal that Angular fires automatically when:
- The
paramschange before the previous request finishes (stale-request cancellation) - The component is destroyed
You can also wire in your own AbortController and combine both signals with AbortSignal.any().
In service we do:
import { inject, Injector, resource, Service, Signal } from '@angular/core';
import { Joke } from './jokes';
import { JOKES_API } from './http-demo-service';
@Service()
export class ResourceDemoService {
// Manual cancellation controller, rebuilt after each abort
#manualAbortController = new AbortController();
abortJokeRequest(reason?: string) {
this.#manualAbortController.abort(reason ?? 'User cancelled');
// Rebuild so the next request gets a fresh signal
this.#manualAbortController = new AbortController();
}
jokeResourceWithAbortSignal = (jokeCount: Signal<number | undefined>) => {
return resource<Joke[], { count: number } | undefined>({
params: () => (jokeCount() !== undefined ? { count: jokeCount()! } : undefined),
defaultValue: [],
loader: async ({ params, abortSignal }) => {
// Log when Angular aborts (param change / destroy)
abortSignal.addEventListener(
'abort',
() => console.log(`Angular aborted: ${abortSignal.reason}`),
{ once: true },
);
// Merge Angular's signal with the manual one
const manualSignal = this.#manualAbortController.signal;
const combinedSignal = AbortSignal.any([abortSignal, manualSignal]);
// Simulate a slow network with a 2-second delay
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(resolve, 2000);
combinedSignal.addEventListener(
'abort',
() => {
clearTimeout(timer);
reject(combinedSignal.reason);
},
{ once: true },
);
});
const response = await fetch(
JOKES_API.GET_JOKES_WITH_DYNAMIC_NUMBERS(params.count),
{ signal: combinedSignal },
);
return await response.json();
},
});
};
}
AbortSignal.any([...signals]): the combined signal fires as soon as any of the input signals abort. It's the cleanest way to merge multiple cancellation sources without manual event listener bookkeeping.
Why rebuild #manualAbortController after each abort?
An AbortController is a one-shot device: once aborted, it stays aborted forever. Every new request needs a fresh controller. Rebuilding it immediately in abortJokeRequest() means the next loader call always gets a clean signal.
In component we do:
import { Component, inject, signal } from '@angular/core';
import { ResourceDemoService } from '../service/resource-demo.service';
@Component({
selector: 'app-jokes-abort',
templateUrl: './jokes-abort.html',
})
export class JokesAbortComponent {
#service = inject(ResourceDemoService);
count = signal<number | undefined>(5);
jokes = this.#service.jokeResourceWithAbortSignal(this.count);
cancel() {
this.#service.abortJokeRequest('User clicked cancel');
}
}
In HTML then we do:
<input
#input
type="number"
placeholder="Enter count (2 sec delay)"
[value]="count()"
(input)="count.set(input.valueAsNumber)"
/>
@if (jokes.isLoading()) {
<p>Fetching... (2 sec delay)</p>
<button (click)="cancel()">Cancel request</button>
} @else if (jokes.error()) {
<p>Cancelled or failed: {{ jokes.error()?.message }}</p>
<button (click)="jokes.reload()">Retry</button>
} @else {
@for (joke of jokes.value() ?? []; track joke.id) {
<p><strong>{{ joke.setup }}</strong>: {{ joke.punchline }}</p>
}
}
Example 5: Streaming with an Async Generator
Every example so far uses loader, and loader has one job which is run once per request and resolve with a single value. Some data does not behave like that though, it comes in over time, one piece at a time, and that is exactly what the stream option on resource() is built for. Instead of a Promise<T>, stream hands back a Signal that keeps updating as new values show up, and the resource's own value() updates right along with it.
In service we do:
import { resource, Service, Signal, signal } from '@angular/core';
import { Joke } from './jokes';
import { JOKES_API } from './http-demo-service';
@Service()
export class ResourceDemoService {
// Async generator: yields one joke at a time with a 2s gap between each.
async *#jokeGenerator(count: number, abortSignal: AbortSignal): AsyncGenerator<Joke> {
for (let i = 0; i < count; i++) {
if (abortSignal.aborted) return;
if (i > 0) {
// Delay between jokes so the streaming effect is visible.
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(resolve, 2000);
abortSignal.addEventListener(
'abort',
() => {
clearTimeout(timer);
reject(abortSignal.reason);
},
{ once: true },
);
});
if (abortSignal.aborted) return;
}
const res = await fetch(JOKES_API.GET_JOKES_WITH_DYNAMIC_NUMBERS(1), { signal: abortSignal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const [joke] = await res.json();
yield joke;
}
}
jokeStreamResource = (jokeCount: Signal<number | undefined>) => {
return resource<Joke[], { count: number } | undefined>({
params: () => {
const count = jokeCount();
return count !== undefined && count > 0 ? { count } : undefined;
},
defaultValue: [],
debugName: 'JokeStreamResource',
stream: async ({ params, abortSignal }) => {
const generator = this.#jokeGenerator(params.count, abortSignal);
// Pull the first joke before returning, keeps the resource in 'loading' until real data arrives.
const { value: firstJoke, done } = await generator.next();
if (done || !firstJoke) throw new Error('No jokes returned');
const accumulated = signal<{ value: Joke[] } | { error: Error }>({ value: [firstJoke] });
// Consume the rest with for await...of, fire and forget in an IIFE.
(async () => {
try {
for await (const joke of generator) {
accumulated.update((prev) => ({
value: [...('value' in prev ? prev.value : []), joke],
}));
}
} catch (err) {
if (!abortSignal.aborted) {
accumulated.set({ error: err instanceof Error ? err : new Error(String(err)) });
}
}
})().then(....);
return accumulated;
},
});
};
}
Quick chat between us, because this deserves a shoutout so JavaScript has had generator functions since ES2015 (function*) and async generators since ES2018 (async function*), and most of us go years without ever writing one. A generator is a function that can pause itself with yield and pick up exactly where it left off the next time you ask it for a value. An async generator is the same idea, except each step can also await something first, which is perfect for logic like "get the next joke, wait a bit, get the next one."
I thought this was a nice chance to show off a feature that exists in plain JS and rarely gets used. Since async generators can await inside them, the natural way to consume one is for await (const joke of generator). It reads just like a normal for...of loop, but under the hood it awaits each yielded value before moving to the next. That is exactly what this demo needed: fetch one joke, wait two seconds, fetch the next, without ever loading the whole batch up front.
Notice the loader waits on generator.next() once before it ever returns the signal. That first await is what keeps the resource in 'loading' status until a real joke actually exists, then it flips to 'resolved' while the rest keep streaming in behind the scenes. Skip that first await and return the signal immediately, and the resource resolves right away with an empty array instead.
In component we do:
import { Component, inject, signal } from '@angular/core';
import { ResourceDemoService } from '../service/resource-demo.service';
@Component({
selector: 'app-jokes-stream',
templateUrl: './jokes-stream.html',
})
export class JokesStreamComponent {
#service = inject(ResourceDemoService);
count = signal<number | undefined>(3);
jokes = this.#service.jokeStreamResource(this.count);
}
In HTML then we do:
<input
#input
type="number"
placeholder="How many jokes to stream?"
[value]="count()"
(input)="count.set(input.valueAsNumber)"
/>
@if (jokes.isLoading()) {
<p>Waiting for the first joke...</p>
} @else if (jokes.error()) {
<p>Something went wrong: {{ jokes.error()?.message }}</p>
}
@for (joke of jokes.value(); track joke.id) {
<div>
<strong>{{ joke.setup }}</strong>
<p>{{ joke.punchline }}</p>
</div>
}
Example 6: Real-Time Data with Server-Sent Events
Not every stream needs a generator behind it. All stream really wants is a Signal that can change over time, so anything that already pushes data at you, like WebSockets, Server-Sent Events, or a Firestore listener, plugs in just as easily. Here is a live feed of real Wikipedia edits over SSE: no generator, no polling, just a native EventSource writing into a signal.
i will use
EventSourcecoz 0 setup config
In service we do:
import { resource, Service, signal } from '@angular/core';
export interface WikiRecentChange {
title: string;
user: string;
comment: string;
wiki: string;
type: 'edit' | 'new' | 'categorize' | 'log';
timestamp: number;
server_url: string;
namespace: number;
length?: { old: number; new: number };
}
const WIKI_STREAM_URL = 'https://stream.wikimedia.org/v2/stream/recentchange';
@Service()
export class ResourceDemoService {
// No params, no reactive dependency: this stream connects once and runs
// until the resource is destroyed. EventSource is closed via abortSignal
// when the component goes away.
wikiEditStreamResource = resource<WikiRecentChange[], Record<never, never>>({
params: () => ({}),
defaultValue: [],
debugName: 'WikiEditStream',
stream: ({ abortSignal }) => {
const editsSignal = signal<{ value: WikiRecentChange[] } | { error: Error }>({ value: [] });
const source = new EventSource(WIKI_STREAM_URL);
// Angular fires abortSignal when the resource is destroyed or reloaded.
// EventSource has no native AbortSignal support, so we close it manually.
abortSignal.addEventListener('abort', () => source.close(), { once: true });
source.onmessage = (event: MessageEvent) => {
if (abortSignal.aborted) {
source.close();
return;
}
try {
const edit = JSON.parse(event.data as string) as WikiRecentChange;
// Filter to main namespace (0) articles only for a cleaner feed.
if (edit.namespace !== 0) return;
editsSignal.update((prev) => ({
value: [edit, ...('value' in prev ? prev.value : [])].slice(0, 20),
}));
} catch {
/* ignore events or handle your own error */
}
};
source.onerror = () => {
if (!abortSignal.aborted) {
editsSignal.set({
error: new Error('EventSource connection lost, reload to reconnect'),
});
}
source.close();
};
return editsSignal;
},
});
}
params: () => ({}) always returns the exact same shape, so Angular never sees a new request and the stream function only runs once. That is the connect-once-and-run-forever pattern, so no reactive params to react to, just a single stream that keeps pushing values until the resource itself is destroyed.
EventSource was not built with AbortSignal in mind, there is no { signal } option like fetch has. That is why the code closes it manually inside an abort event listener instead. Worth remembering any time you wrap a non-fetch API in stream: check what that API uses for cancellation, and bridge Angular's abortSignal into it yourself.
Thanks to @_W4XUmu3QzCEcPNElv8dLg for reviewing and dusscssing this part together
Worth knowing before you copy this pattern elsewhere, signals never buffer, no matter which method you write with. resource.value() is just a plain computed() reading whatever signal stream hands back, so if that signal gets written twice before anything reads it, only the second write ever existed as far as anyone downstream is concerned.
Switching from .set() to .update() does not fix this by itself. What actually matters is whether your update function reads the previous value and folds it into the new one. Look at what this example already does above: editsSignal.update((prev) => ({ value: [edit, ...prev.value].slice(0, 20) })) takes whatever was already there and carries it forward into the new value. That is the part doing the work, not the method name.
This is also a different problem from the stale request cancellation in the AbortSignal example. That one is about several independent network requests racing each other, solved with abortSignal. This one is about a single that being overwritten before anything looked at it, and the fix is how you write to that signal, not how you cancel anything.
In component we do:
import { Component, inject } from '@angular/core';
import { ResourceDemoService } from '../service/resource-demo.service';
@Component({
selector: 'app-wiki-feed',
templateUrl: './wiki-feed.html',
})
export class WikiFeedComponent {
#service = inject(ResourceDemoService);
edits = this.#service.wikiEditStreamResource;
}
In HTML then we do:
@if (edits.isLoading()) {
<p>Connecting to the live feed...</p>
} @else if (edits.error()) {
<p>{{ edits.error()?.message }}</p>
}
@for (edit of edits.value(); track edit.timestamp) {
<div>
<strong>{{ edit.title }}</strong> ({{ edit.wiki }})
<p>{{ edit.user }}: {{ edit.comment }}</p>
</div>
}
Quick Comparison
| Pattern | Has params? |
Signal-reactive? | Skippable? | AbortSignal? | Streams over time? |
|---|---|---|---|---|---|
| Pure form | No | No | No | No | No |
| Async/await + params | Yes | Yes | Partial (guard in loader) | No | No |
| Conditional loading | Yes | Yes | Yes (return undefined from params) |
No | No |
| AbortSignal | Yes | Yes | Yes | Yes | No |
| Async generator stream | Yes | Yes | Yes | Yes | Yes |
| SSE stream | Yes (static) | No | No | Yes | Yes |
Resource Signal API
Verified against the actual Angular
Resource<T>,WritableResource<T>, andResourceRef<T>type definitions.
All members below are properties on the ResourceRef<T> returned by resource(). Each Signal<X> means you call it as a function to read the current value.
// ββ Signals (call as a function to read) ββββββββββββββββββββββββββββββ
resource.value() // T | undefined: throws in 'error' state; always guard with hasValue()
resource.status() // ResourceStatus: current lifecycle state (see table below)
resource.error() // Error | undefined: last known error; only set in 'error' status
resource.isLoading() // boolean: true during both 'loading' and 'reloading'
resource.snapshot() // ResourceSnapshot<T>: { status, value, error } as a plain object
// ββ Methods βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
resource.hasValue() // boolean: true only when status is 'resolved' or 'local' with a value
resource.reload() // boolean: true if reload was initiated; false if unnecessary
resource.set(value) // void: write a local value (moves status to 'local')
resource.update(fn) // void: update local value with a function (moves status to 'local')
resource.asReadonly() // Resource<T>: read-only view; strips set/update/reload/destroy
resource.destroy() // void: cancel in-flight request and return to 'idle'
Common mistake: resource.error is Signal<Error | undefined> on the public interface: calling resource.error() returns Error | undefined, not the signal itself. The error?: Error you may find inside Angular's source is on the internal WrappedRequest type (private implementation detail), not the public API.
Where ResourceStatus is:
type ResourceStatus = 'idle' | 'error' | 'loading' | 'reloading' | 'resolved' | 'local';
| Status | What it means |
|---|---|
idle |
params returned undefined: no request, no value |
loading |
Loader is running for the first time, value() is undefined
|
reloading |
Loader re-running (param change / manual reload), value() still holds previous data |
resolved |
Loader completed, value() has the data |
error |
Loader threw or the promise rejected, value() throws if called |
local |
Value was set manually via .set() or .update()
|
'reloading' is the UX gem: value() still returns stale data while the new fetch runs, so you can show a subtle spinner on top of existing content instead of a full loading screen.
Safe Value Access: Avoiding the Runtime Error
Calling resource.value() when the resource is in an error state throws at runtime. Always guard reads with hasValue(). This is called out explicitly in the official Angular docs.
See https://angular.dev/guide/signals/resource
Option 1: Guard in the template with hasValue()
@if (jokes.hasValue()) {
@for (joke of jokes.value(); track joke.id) {
<p>{{ joke.setup }}</p>
}
} @else if (jokes.error()) {
<p>Something went wrong.</p>
} @else if (jokes.isLoading()) {
<p>Loading...</p>
}
Option 2: Guard in the component with computed()
Keeps the template clean and lets you pass the value to child components or derive further state from it.
import { computed, inject } from '@angular/core';
export class JokesComponent {
#service = inject(ResourceDemoService);
jokes = this.#service.pureJokeDataResource;
jokesValue = computed(() =>
this.jokes.hasValue() ? this.jokes.value() : undefined
);
}
Then in the template:
@if (jokesValue()) {
@for (joke of jokesValue()!; track joke.id) {
<p>{{ joke.setup }}</p>
}
} @else if (jokes.error()) {
<p>Something went wrong.</p>
}
Which one to pick
| Approach | Best for |
|---|---|
hasValue() in template |
Simple components, straightforward conditional rendering |
computed() with hasValue()
|
Reusing the value in multiple places, passing it to child components, or deriving further state |
Follow me for more
Follow me for more
Twitter / X | LinkedIn | Facebook | Bluesky

Top comments (0)