π Angular's `linkedSignal` and `resource()`: A Practical Guide for 2026
Hi π, I'm Tushar Patil. Currently I am working as Frontend Developer (Angular) and also have expertise with .Net Core and Framework.
π― Why these two APIs matter right now
Angular's Signals story has been building for three years. With Angular 20, the final pieces clicked into place: linkedSignal() and resource() both graduated to stable, and together they solve two problems that every Angular developer hits repeatedly.
The first problem is dependent writable state β you need a signal whose initial value is derived from another signal, but the user can also change it directly. computed() is read-only, so it's no help. Before linkedSignal(), developers reached for awkward effect() workarounds or manual synchronization logic. π©
The second problem is async data in a signals world β you have a reactive fetch that should re-run automatically when params change, surface loading/error states as signals, and cancel in-flight requests when the params change again. Previously this meant wiring up RxJS observables, managing subscriptions, and handling race conditions by hand. resource() handles all of that declaratively. π
Let's look at each one in depth.
π Part 1: linkedSignal()
π€ The problem it solves
Suppose you have a shipping options component. The available options come from a server (or a parent component as an input), and you want to pre-select the first one. But the user can also pick a different option themselves.
Before linkedSignal(), the typical approach was to use a signal() for the selection and an effect() to reset it when the options changed. This works, but effect() is meant for side effects β using it for state derivation is a code smell that the Angular team actively warns against. π©
linkedSignal() is the right tool: it creates a writable signal whose value resets automatically when its reactive source changes. β¨
π Basic usage
import { Component, Signal, linkedSignal } from '@angular/core';
@Component({ /* ... */ })
export class ShippingPickerComponent {
shippingOptions: Signal<string[]> = getShippingOptions();
// β
Automatically initializes to the first option.
// If shippingOptions changes (e.g., user switches region),
// selectedOption resets to the new first element.
selectedOption = linkedSignal(() => this.shippingOptions()[0]);
selectOption(index: number) {
this.selectedOption.set(this.shippingOptions()[index]);
}
}
The key difference from computed(): selectedOption is a WritableSignal. The user can call .set() on it, and that change holds β until shippingOptions changes, at which point the computation re-runs and resets it to the new first element. π
π§ The advanced form: preserving state across resets
Sometimes you don't want a full reset. Say the user has selected "Air" shipping βοΈ, and then the available options refresh with a new list that still includes "Air". Resetting to the first item would be a bad UX β you should keep their selection if it's still valid.
The advanced linkedSignal() form gives you access to the previous value:
interface ShippingMethod {
id: number;
name: string;
}
selectedOption = linkedSignal<ShippingMethod[], ShippingMethod>({
source: () => this.shippingOptions(),
computation: (newOptions, previous) => {
// π If the user had a selection and it still exists in the new list, keep it
if (previous) {
const stillAvailable = newOptions.find(
opt => opt.id === previous.value.id
);
if (stillAvailable) return stillAvailable;
}
// Otherwise default to the first option
return newOptions[0];
}
});
The computation function receives:
sourceβ the new value of the source signalpreviousβ an object withprevious.source(old source value) andprevious.value(the previouslinkedSignalvalue)
π Note: When using the
previousparameter, you must provide generic type arguments explicitly:linkedSignal<SourceType, ValueType>.
π Using linkedSignal() for editable form copies
This is the most common real-world use case. You load data from the server, display it in a form, and let the user edit it. When the user saves, you send the edited copy. If the source data reloads (e.g., another user changed it), the form should refresh. π
@Component({ /* ... */ })
export class UserProfileComponent {
loadedUser = this.store.user; // a Signal<User>
// βοΈ Editable local copies β reset if loadedUser changes
name = linkedSignal(() => this.loadedUser().name);
email = linkedSignal(() => this.loadedUser().email);
role = linkedSignal(() => this.loadedUser().role);
save() {
this.store.update({
...this.loadedUser(),
name: this.name(),
email: this.email(),
role: this.role(),
});
}
}
The template uses two-way binding directly on the linked signals:
<input [(ngModel)]="name" />
<input [(ngModel)]="email" />
<select [(ngModel)]="role"> ... </select>
<button (click)="save()">Save</button>
The user can freely edit the form. If the server pushes an update to loadedUser, all three fields reset to reflect the new data. Clean, and zero effect() calls. πͺ
π« When not to use linkedSignal()
Use computed() when the derived value should be strictly read-only β for example, a total price derived from quantity and unit price. There's no scenario where you'd want the user to override that value directly. linkedSignal() is specifically for cases where manual overrides are part of the intended UX.
π‘ Part 2: resource()
π© The problem it solves
Before resource(), fetching data reactively in Angular typically meant:
- π Injecting
HttpClient - π Using
switchMapin an RxJS pipeline to react to param changes - π Manually tracking loading and error states
- π§Ή Unsubscribing on destroy
- π Handling race conditions manually (usually via
switchMap)
That's a lot of boilerplate for something as universal as "fetch data when this signal changes." resource() replaces the whole pattern. π
βοΈ How resource() works
A resource has two parts:
paramsποΈ β a reactive function (likecomputed()) that returns the parameters for the async operation. When the params change, the resource automatically re-fetches.loaderπ β an async function that receives the current params and returns aPromise.
import { resource, signal } from '@angular/core';
userId = signal<string>('user-1');
userResource = resource({
params: () => ({ id: this.userId() }),
loader: ({ params }) => fetch(`/api/users/${params.id}`)
.then(res => res.json())
});
The resource automatically exposes the following signals:
| Signal | Type | Description |
|---|---|---|
.value() β
|
T | undefined |
The resolved data |
.isLoading() β³ |
boolean |
True while the loader is running |
.error() β |
unknown |
The thrown error, if any |
.hasValue() π |
boolean |
True when data is available |
.status() π |
ResourceStatus |
Full status string |
πΌοΈ Using resource signals in templates
@if (userResource.isLoading()) {
<p>β³ Loading...</p>
} @else if (userResource.error()) {
<p>β Error: could not load user.</p>
} @else if (userResource.hasValue()) {
<user-profile [user]="userResource.value()" />
}
β οΈ Always guard
.value()withhasValue()β reading.value()when the resource is in an error state throws at runtime.
π rxResource() β the RxJS-friendly variant
If your data layer uses HttpClient (which returns Observable), use rxResource() from @angular/core/rxjs-interop. It's identical to resource() except the loader function returns an Observable instead of a Promise. As of Angular 20, the loader key is called stream in rxResource().
import { inject, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
@Component({ /* ... */ })
export class ProductListComponent {
private http = inject(HttpClient);
page = signal(1);
productsResource = rxResource({
params: () => this.page(),
stream: ({ params: page }) =>
this.http.get<Product[]>(`/api/products?page=${page}`)
});
}
rxResource() automatically unsubscribes and cancels in-flight requests when params change β no switchMap, no takeUntilDestroyed. π§
β‘ httpResource() β the simplest option for plain HTTP GET
For the most common case β a reactive HTTP GET with a dynamic URL β httpResource() is the most concise API:
import { httpResource } from '@angular/core';
@Component({ /* ... */ })
export class UserComponent {
userId = input.required<string>();
// π Automatically re-fetches whenever userId changes
user = httpResource(() => `/api/users/${this.userId()}`);
}
httpResource() runs through Angular's HttpClient stack, including interceptors, and returns the response as signals. It also supports response type variants (.text(), .blob(), .arrayBuffer()) and schema validation via Zod or Valibot via a parse option.
π§ͺ Note:
httpResource()is still experimental as of Angular 21.resource()andrxResource()are stable.
π€ Combining resource() with linkedSignal()
This is where the two APIs shine together. β¨ Say you fetch a list of posts and want to track the selected post β defaulting to the first one, but letting the user pick:
@Component({ /* ... */ })
export class PostBrowserComponent {
private http = inject(HttpClient);
category = signal('angular');
postsResource = rxResource({
params: () => this.category(),
stream: ({ params: cat }) =>
this.http.get<Post[]>(`/api/posts?category=${cat}`)
});
// π― Defaults to first post, resets when posts reload,
// but user can select any post manually
selectedPost = linkedSignal(
() => this.postsResource.value()?.[0] ?? null
);
selectPost(post: Post) {
this.selectedPost.set(post);
}
}
When category changes, postsResource re-fetches, and selectedPost automatically resets to the new first post. The user can select any post in between, and that selection holds until the next category change. π
π οΈ Part 3: Common patterns and pitfalls
π Race conditions are handled for you
One of the most important things resource() does behind the scenes: if params change while a load is already in flight, the previous request is aborted (for resource() via AbortSignal, and for rxResource() via automatic unsubscription). You never need to think about stale responses overwriting fresh ones. π‘οΈ
π Chained resources
Resources can depend on other resources via computed():
userResource = resource({
params: () => ({ id: this.userId() }),
loader: ({ params }) => fetchUser(params)
});
// βοΈ Only triggers when userResource has a value
profilePicResource = httpResource(
() => this.userResource.hasValue()
? `/api/images/${this.userResource.value()!.profilePicId}`
: undefined
);
When the params function returns undefined, the resource stays in idle state and doesn't fire a request. π΄
π Manual reload
Resources expose a .reload() method for cases like "pull to refresh":
<button (click)="postsResource.reload()">π Refresh</button>
π« Avoid using resource() for mutations
resource() is designed for read operations only. If params change while a POST or PUT is in flight, the request gets cancelled β meaning your mutation might never reach the server. β οΈ Use HttpClient directly (or a dedicated mutations pattern) for writes.
π Summary: which API to reach for
| Scenario | Use |
|---|---|
| Derive a value reactively, user can override it ποΈ | linkedSignal() |
| Editable form copy synced with server data π | linkedSignal() |
| Fetch data reactively using Promise π€ | resource() |
| Fetch data reactively using HttpClient/Observable π | rxResource() |
| Simple reactive HTTP GET, don't need full control β‘ | httpResource() |
| Strict read-only derived value π | computed() |
| Side effects (logging, DOM, non-Angular APIs) βοΈ | effect() |


