On this page

Effects, linkedSignal, and resource

12 min read TextCh. 2 — Reactivity

Effects: reactive side effects

An effect() runs a function every time the signals it reads change. It is the tool for side effects: syncing with localStorage, sending analytics, logging, etc.

import { signal, effect } from '@angular/core';

const theme = signal<'light' | 'dark'>('dark');

// Runs on creation and every time theme() changes
effect(() => {
  document.documentElement.setAttribute('data-theme', theme());
});

Rules for effects

  1. Only in injection context — Inside a constructor, a component field, or runInInjectionContext
  2. Cleaned up automatically — When the component is destroyed
  3. Should not write to signals — Except with allowSignalWrites (not recommended)

Cleanup in effects

If your effect registers listeners or timers, you can clean them up with onCleanup:

effect((onCleanup) => {
  const id = setInterval(() => {
    console.log('Tick for:', userId());
  }, 1000);

  onCleanup(() => clearInterval(id));
});

linkedSignal: writable derived signal

linkedSignal creates a signal that is automatically computed when its source changes, but that you can also write to manually:

import { signal, linkedSignal } from '@angular/core';

const categoryId = signal(1);

// Resets to 1 every time categoryId changes
const page = linkedSignal<number>({
  source: categoryId,
  computation: () => 1,
});

// You can write to it manually
page.set(3);

// But when categoryId changes, it resets to 1
categoryId.set(2); // page() === 1

Typical use cases:

  • Pagination that resets when filters change
  • Active tab that returns to the first one when navigating
  • Forms that reset when selecting a different record

resource: reactive async loading

The resource API connects signals with asynchronous operations. When dependencies change, the resource reloads automatically:

import { signal, resource } from '@angular/core';

const search = signal('angular');

const results = resource({
  request: () => ({ q: search() }),
  loader: async ({ request, abortSignal }) => {
    const resp = await fetch(
      `https://api.example.com/search?q=${request.q}`,
      { signal: abortSignal },
    );
    return resp.json();
  },
});

Resource properties

Property Type Description
.value() T | undefined The loaded value
.isLoading() boolean Whether it is loading
.error() unknown The error, if one occurred
.status() ResourceStatus Detailed status
.reload() void Manually reload

rxResource for Observables

If you prefer working with Observables (e.g., HttpClient), use rxResource:

import { rxResource } from '@angular/core/rxjs-interop';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

const http = inject(HttpClient);
const id = signal(1);

const data = rxResource({
  request: () => ({ id: id() }),
  loader: ({ request }) => http.get(`/api/data/${request.id}`),
});

Practice

  1. Create a persistence effect: Implement a signal for the theme ('light' | 'dark') and an effect() that saves the value to localStorage every time it changes.
  2. Use linkedSignal: Create a categoryId signal and a linkedSignal for the current page that resets to 1 whenever the category changes. Add buttons to switch categories and advance pages.
  3. Implement a resource: Use the resource API to reactively load data from https://jsonplaceholder.typicode.com/users/{id}. Display loading, error, and data states with @if.

In the next lesson, we will see how components communicate with each other using inputs, outputs, and model.

Don't overuse effect()
Effects should be used for side effects (localStorage, analytics, logs). If you only need to derive a value, use computed(). If you need a derived signal that you can also write to, use linkedSignal().
resource cancels automatically
When a resource's dependencies change, the previous request is automatically canceled via AbortSignal. You don't need to manage cancellation manually.
import {
  Component, signal, computed, effect,
  linkedSignal, resource,
  ChangeDetectionStrategy,
} from '@angular/core';

interface User {
  id: number;
  name: string;
  email: string;
}

@Component({
  selector: 'app-profile',
  changeDetection: ChangeDetectionStrategy.OnPush,
  templateUrl: './profile.html',
})
export class Profile {
  readonly userId = signal(1);

  // linkedSignal: writable derived signal
  readonly activeTab = linkedSignal<string>({
    source: this.userId,
    computation: () => 'info',  // resets when user changes
  });

  // resource: reactive async loading
  readonly userResource = resource<User, { id: number }>({
    request: () => ({ id: this.userId() }),
    loader: async ({ request, abortSignal }) => {
      const resp = await fetch(
        `https://api.example.com/users/${request.id}`,
        { signal: abortSignal },
      );
      if (!resp.ok) throw new Error('Failed to load user');
      return resp.json();
    },
  });

  // Access to resource state
  readonly user = computed(() => this.userResource.value());
  readonly loading = computed(() => this.userResource.isLoading());
  readonly error = computed(() => this.userResource.error());

  constructor() {
    // Effect to sync with local storage
    effect(() => {
      const id = this.userId();
      localStorage.setItem('lastUser', String(id));
      console.log('Selected user:', id);
    });
  }

  changeUser(id: number): void {
    this.userId.set(id);
  }

  changeTab(tab: string): void {
    this.activeTab.set(tab);
  }
}