Skip to main content

Command Palette

Search for a command to run...

πŸ”— Angular's `linkedSignal` and `resource()`: A Practical Guide for 2026

Published
β€’9 min read
πŸ”— Angular's `linkedSignal` and `resource()`: A Practical Guide for 2026
T

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 signal
  • previous β€” an object with previous.source (old source value) and previous.value (the previous linkedSignal value)

πŸ“ Note: When using the previous parameter, 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:

  1. πŸ’‰ Injecting HttpClient
  2. πŸ”€ Using switchMap in an RxJS pipeline to react to param changes
  3. πŸ“Š Manually tracking loading and error states
  4. 🧹 Unsubscribing on destroy
  5. 🏁 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 (like computed()) 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 a Promise.
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() with hasValue() β€” 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() and rxResource() 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()

πŸ“š Further reading

More from this blog

T

Tushar's Blog

12 posts

Hi πŸ‘‹, I'm Tushar Patil. Currently I am working as Frontend Developer (Angular) and also have expertise with .Net Core and Framework.