You already know signal() and computed(). This lesson covers the two tools for harder cases: effect() for side effects and linkedSignal() for state that depends on another signal but can still be changed by the user.
What you will learn
- What
effect()is for, and when not to use it - How
linkedSignal()creates resettable, writable state - How
untracked()stops unwanted dependencies
effect(): run code when signals change
An effect runs once at the start and again whenever a signal it reads changes. Use it to connect signals to things outside Angular: saving to storage, logging, drawing on a canvas, or calling a non-Angular library.
// src/app/advanced/theme-saver-effect.ts
import { Component, effect, signal } from '@angular/core';
@Component({
selector: 'app-theme-effect',
template: `
<button type="button" (click)="toggle()">Theme: {{ theme() }}</button>
`,
})
export class ThemeEffect {
protected readonly theme = signal<'light' | 'dark'>('light');
constructor() {
// Runs now, and each time theme() changes.
effect(() => {
document.documentElement.dataset['theme'] = this.theme();
try {
localStorage.setItem('theme', this.theme());
} catch {
// storage can be blocked
}
});
}
protected toggle(): void {
this.theme.update((t) => (t === 'light' ? 'dark' : 'light'));
}
}
Effects must be created in an injection context, such as a constructor or a field initializer. Angular destroys them together with the component.
### Do not use effect() to copy state
This is the most common mistake:
// Avoid: an effect that sets another signal
effect(() => {
this.total.set(this.price() * this.quantity());
});
// Prefer: derived state with computed()
readonly total = computed(() => this.price() * this.quantity());
computed() is lazy, cached and easy to follow. Use an effect only when something outside the signal graph must happen.
linkedSignal(): derived but writable
Sometimes state depends on another signal, yet the user can change it. Example: a list of shipping options for a country, where the selected option must reset when the country changes.
// src/app/advanced/shipping-picker.ts
import { Component, linkedSignal, signal } from '@angular/core';
const OPTIONS: Record<string, string[]> = {
India: ['Standard', 'Express'],
Germany: ['Standard', 'Express', 'Pickup'],
};
@Component({
selector: 'app-shipping-picker',
template: `
<label for="country">Country</label>
<select id="country" (change)="country.set($any($event.target).value)">
@for (c of countries; track c) {
<option [value]="c">{{ c }}</option>
}
</select>
<fieldset>
<legend>Shipping</legend>
@for (o of options(); track o) {
<label>
<input type="radio" name="ship" [checked]="selected() === o" (change)="selected.set(o)" />
{{ o }}
</label>
}
</fieldset>
<p>Selected: {{ selected() }}</p>
`,
})
export class ShippingPicker {
protected readonly countries = Object.keys(OPTIONS);
protected readonly country = signal('India');
protected readonly options = linkedSignal(() => OPTIONS[this.country()]);
// Starts as the first option, resets whenever options change,
// but the user can still call selected.set(...).
protected readonly selected = linkedSignal<string[], string>({
source: this.options,
computation: (list, previous) =>
previous && list.includes(previous.value) ? previous.value : list[0],
});
}
The long form takes a source, and a computation that also receives the previous value. Here we keep the old choice if it still exists, and fall back to the first option otherwise.
untracked(): read without depending
Inside an effect or computed, untracked() reads a signal without making it a dependency:
// src/app/advanced/search-logger.ts
import { Component, effect, signal, untracked } from '@angular/core';
@Component({
selector: 'app-search-logger',
template: `<input aria-label="Search" (input)="query.set($any($event.target).value)" />`,
})
export class SearchLogger {
protected readonly query = signal('');
private readonly clicks = signal(0);
constructor() {
effect(() => {
const q = this.query(); // tracked: the effect re-runs when this changes
const n = untracked(() => this.clicks()); // not tracked
console.log(`Search "${q}" after ${n} clicks`);
});
}
}
The key parts
- Prefer
computed()for derived values. UselinkedSignal()for derived values the user can override. Useeffect()only for side effects outside Angular. - Effects run asynchronously during change detection, not at the moment a signal is set.
- Avoid setting signals inside effects. If you must, think again about
computedorlinkedSignal. untracked()is the escape hatch for values you want to read but not follow.
Try it yourself
- Add a "reset" button to the shipping picker that sets
selectedback to the first option. - Rewrite the theme effect so it also reads the saved theme at start-up.
- Find one place in your own code where an effect could be a
computed().
Quick check
- When is
effect()the right tool? - What is different about
linkedSignal()compared withcomputed()? - What does
untracked()do?
Next, load data that depends on signals with the resource API. See the signals guide for more.