On this page
Effects, linkedSignal, and resource
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
- Only in injection context — Inside a constructor, a component field, or
runInInjectionContext - Cleaned up automatically — When the component is destroyed
- 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() === 1Typical 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
- Create a persistence effect: Implement a signal for the theme (
'light' | 'dark') and aneffect()that saves the value tolocalStorageevery time it changes. - Use linkedSignal: Create a
categoryIdsignal and alinkedSignalfor the current page that resets to 1 whenever the category changes. Add buttons to switch categories and advance pages. - Implement a resource: Use the
resourceAPI to reactively load data fromhttps://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.
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);
}
}
Sign in to track your progress