# Angular Signals π
> Learn Angular signals in v22 β signal(), set/update, computed, reactive contexts, untracked, equality, asReadonly and how signals drive change detection.
Source: https://ng-guide.letsprogram.in/modern/signals/
A **signal** is a box around a value that tells Angular when the value changes. You read it by calling it like a function, and Angular remembers *who* read it. When you change the value, only the templates, `computed()`s and effects that actually read it are updated. Signals are the foundation of modern Angular: inputs, queries, forms, resources and zoneless change detection are all built on them.
## What you'll learn
- Create, read and update writable signals with `signal()`, `set()` and `update()`
- Derive values with `computed()` and expose read-only state with `asReadonly()`
- Understand reactive contexts and when Angular tracks a read
- Read without tracking using `untracked()`
- Control change notifications with custom equality functions
- Avoid the common mistakes (mutating objects, async reads, effects for derived state)
## Your first signal
```ts title="counter.ts"
import { Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
Count: {{ count() }}
Double: {{ double() }}
`,
})
export class Counter {
protected readonly count = signal(0);
protected readonly double = computed(() => this.count() * 2);
protected increment() {
this.count.update((c) => c + 1);
}
protected reset() {
this.count.set(0);
}
}
```
Three things to notice:
1. **Read** a signal by calling it: `count()` (in the template *and* in TypeScript).
2. **Write** with `set(newValue)` or `update(old => new)`.
3. The template updates automatically. The component is OnPush (the default since v22) and there is no zone.js β the signal tells Angular exactly which view to refresh.
## Writable signals
`signal(initialValue)` returns a `WritableSignal`. The type is inferred, or you can pass it explicitly:
```ts
import { signal, WritableSignal } from '@angular/core';
const count = signal(0); // WritableSignal
const name = signal(null); // explicit type
const tags: WritableSignal = signal([]);
console.log(count()); // read: 0
count.set(3); // replace the value
count.update((c) => c + 1); // compute from the previous value β 4
```
| Method | Use it when |
|---|---|
| `set(value)` | You know the new value (`name.set('Ada')`) |
| `update(fn)` | The new value depends on the old one (`count.update(c => c + 1)`) |
| `asReadonly()` | You want to hand out a read-only view (see below) |
### Objects and arrays: always create a new value
Signals compare values with `Object.is()` by default. If you mutate an object in place, the reference doesn't change, so **nothing updates**. Always produce a new object or array:
```ts title="todo-list.ts"
import { Component, computed, signal } from '@angular/core';
interface Todo {
id: number;
title: string;
done: boolean;
}
@Component({
selector: 'app-todo-list',
template: `
@for (todo of todos(); track todo.id) {
}
{{ remaining() }} left
`,
})
export class TodoList {
protected readonly todos = signal([{ id: 1, title: 'Learn signals', done: false }]);
protected readonly remaining = computed(() => this.todos().filter((t) => !t.done).length);
protected add(title: string) {
this.todos.update((list) => [...list, { id: Date.now(), title, done: false }]);
}
protected toggle(id: number) {
this.todos.update((list) => list.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
}
}
```
```ts
// β Mutation: same array reference, no update is triggered
this.todos().push(newTodo);
// β New array
this.todos.update((list) => [...list, newTodo]);
```
> **Note**
>
> Older previews had a `mutate()` method. It was removed before signals became stable β use `update()` with an immutable change.
## Computed signals
`computed()` creates a **read-only** signal derived from other signals. It is lazy (runs only when read) and memoized (re-runs only when a dependency changed):
```ts
import { computed, signal } from '@angular/core';
const price = signal(100);
const quantity = signal(2);
const total = computed(() => price() * quantity()); // Signal
console.log(total()); // 200
quantity.set(3);
console.log(total()); // 300
```
Use `computed()` for anything you can *calculate* from other state: filtered lists, totals, flags like `isValid`. It has its own page: [Computed Signals](/modern/computed-signals/).
## Exposing read-only state with `asReadonly()`
A common pattern is a service that owns writable state but only lets others read it:
```ts title="cart-store.ts"
import { Service, computed, signal } from '@angular/core';
@Service()
export class CartStore {
private readonly _items = signal([]);
readonly items = this._items.asReadonly(); // Signal
readonly count = computed(() => this._items().length);
add(item: string) {
this._items.update((items) => [...items, item]);
}
}
```
```ts title="cart-badge.ts"
import { Component, inject } from '@angular/core';
import { CartStore } from './cart-store';
@Component({
selector: 'app-cart-badge',
template: `π {{ cart.count() }}`,
})
export class CartBadge {
protected readonly cart = inject(CartStore);
}
```
Consumers can call `cart.items()` but not `cart.items.set(...)` β that is a compile error. The read-only signal always reflects the latest value of the original.
> **Caution**
>
> `asReadonly()` prevents `set()`/`update()`, but it does **not** deep-freeze the value. Code could still mutate the array it reads. Treat signal values as immutable.
## Signals you already use
Many Angular APIs return signals, so everything on this page applies to them too:
| API | Returns |
|---|---|
| `input()`, `input.required()` | `InputSignal` (read-only) |
| `model()` | `ModelSignal` (writable, two-way bindable) |
| `viewChild()`, `contentChild()`, `viewChildren()`, `contentChildren()` | read-only signals |
| `computed()` | `Signal` |
| `linkedSignal()` | `WritableSignal` β see [Linked Signals](/modern/linked-signals/) |
| `resource()`, `httpResource()` | `value`, `status`, `error`, `isLoading` signals β see [Resource API](/modern/resource-api/) |
| `toSignal(observable$)` | `Signal` from RxJS β see [RxJS & Angular patterns](/rxjs/angular-patterns/) |
| `form()` (Signal Forms) | field state signals β see [Signal Forms](/modern/signal-forms/) |
## Reactive contexts: when are reads tracked?
A **reactive context** is code that Angular watches. Any signal you read synchronously inside it becomes a dependency. Angular creates a reactive context when it:
- renders a component template (including `host` bindings),
- evaluates a `computed()` or `linkedSignal()`,
- runs an `effect()` or `afterRenderEffect()`,
- evaluates a resource's `params` function.
Outside those places (event handlers, `ngOnInit`, plain methods called from a click) reading a signal just returns its value β nothing is tracked.
Dependencies are **dynamic**: only signals actually read during the *last* run are tracked.
```ts
const showCount = signal(false);
const count = signal(0);
const message = computed(() => (showCount() ? `The count is ${count()}` : 'Nothing to see here'));
// While showCount() is false, changing count() does NOT recompute `message`.
```
### Reads after `await` are not tracked
Tracking only works for synchronous code. Read your signals **before** the first `await`:
```ts
// β theme() is read after await β not a dependency
effect(async () => {
const data = await fetchUserData();
console.log(data.name, theme());
});
// β read first, then await
effect(async () => {
const currentTheme = theme();
const data = await fetchUserData();
console.log(data.name, currentTheme);
});
```
### Reading without tracking: `untracked()`
Sometimes you need a signal's current value inside a reactive context but don't want changes to it to re-run your code. Wrap the read with `untracked()`:
```ts
import { effect, signal, untracked } from '@angular/core';
const currentUser = signal('ada');
const counter = signal(0);
// Re-runs only when currentUser changes, not when counter changes
effect(() => {
console.log(`User: ${currentUser()}, counter: ${untracked(counter)}`);
});
// Also useful to call code that might read signals internally
effect(() => {
const user = currentUser();
untracked(() => {
logger.log(`User changed to ${user}`); // signal reads inside log() aren't tracked
});
});
```
If you write a helper that must never run in a reactive context (for example because it subscribes to something), guard it with `assertNotInReactiveContext(myHelper)`; it throws a clear error if someone calls it from a `computed` or template.
## Equality functions
By default a signal only notifies when the new value is not `Object.is()`-equal to the old one. Setting the same primitive twice does nothing:
```ts
const n = signal(1);
n.set(1); // no notification
```
You can supply your own `equal` function to `signal()`, `computed()` and `linkedSignal()`:
```ts
interface User {
id: number;
name: string;
}
// Treat users with the same id as "the same" value
const user = signal({ id: 1, name: 'Ada' }, { equal: (a, b) => a.id === b.id });
user.set({ id: 1, name: 'Ada Lovelace' }); // equal β no update!
```
> **Caution**
>
> A custom `equal` that returns `true` means the new value is **ignored for notification purposes** β dependents keep showing the old one. Only use it when "equal" really means "nothing visible changed" (for example a deep-equal check on arrays).
## Debugging helpers
```ts
import { isSignal, isWritableSignal, signal, computed } from '@angular/core';
const clicks = signal(0, { debugName: 'clicks' }); // name shown in Angular DevTools
const doubled = computed(() => clicks() * 2, { debugName: 'doubled' });
isSignal(clicks); // true
isWritableSignal(doubled); // false
```
Angular DevTools can display the **signal graph** of a component β which signals, computeds and effects depend on each other. See [Angular DevTools](/tools/devtools/).
## Debouncing a signal (experimental)
`debounced(source, ms)` turns a fast-changing signal (like a search box) into a **resource** whose value only settles after the source stops changing:
```ts title="search.ts"
import { Component, debounced, resource, signal } from '@angular/core';
@Component({
selector: 'app-search',
template: `
@if (results.isLoading()) {
Searchingβ¦
}
@for (item of results.value(); track item.id) {
{{ item.name }}
}
`,
})
export class Search {
protected readonly query = signal('');
protected readonly debouncedQuery = debounced(this.query, 300);
protected readonly results = resource({
params: () => this.debouncedQuery.value(),
loader: ({ params }) => fetchResults(params),
defaultValue: [],
});
}
```
While the timer is running, `debouncedQuery.status()` is `'loading'` and `value()` keeps the previous settled value. It must be created in an injection context (or given `{ injector }`), and Angular cancels the timer automatically when the component is destroyed.
> **Experimental**
>
> `debounced()` was added in v22.0 as **experimental** β the API may change in a minor release. For forms, use the stable `debounce()` rule from Signal Forms instead.
## Signals and change detection
When a template reads a signal, Angular registers that component's view as a consumer. When the signal changes, Angular marks **that component** for refresh and schedules change detection β components whose signals didn't change are skipped. That is why signals work so well with:
- **OnPush** components (the default since v22) β no need for `markForCheck()`.
- **Zoneless** apps (the default since v21) β signals are one of the notifications Angular listens to. See [Zoneless Angular](/modern/zoneless/).
## Common mistakes
| Mistake | Why it's a problem | Fix |
|---|---|---|
| `{{ count }}` instead of `{{ count() }}` | Prints the function, not the value | Call the signal |
| `this.items().push(x)` | Mutation doesn't change the reference β no update | `this.items.update(l => [...l, x])` |
| Copying one signal into another with `effect()` | Extra change detection cycles, glitches, loops | Use `computed()` or `linkedSignal()` |
| Reading signals after `await` in an effect | Not tracked | Read them first |
| Using a custom `equal` that's too loose | Real changes are swallowed | Keep default equality unless you're sure |
| Creating signals inside a template expression or getter | New signal every check | Create signals once as class fields |
## Version notes
- Signals (`signal`, `computed`) became stable in v17; `effect`, `linkedSignal` and `toSignal` in v20.
- Resources (`resource`, `rxResource`, `httpResource`) became stable in v22.
- `debounced()` and `resourceFromSnapshots()` are experimental in v22.
## Key takeaways
- `signal()` holds state; call it to read, `set()`/`update()` to write.
- `computed()` derives state lazily and caches it; prefer it over effects.
- Only synchronous reads inside reactive contexts are tracked; use `untracked()` to opt out.
- Treat values as immutable β new objects/arrays on every change.
- Expose state with `asReadonly()` from services.
## Next steps
- [Computed Signals](/modern/computed-signals/) β derived state in depth
- [Effects](/modern/effects/) β side effects, and when *not* to use them
- [Linked Signals](/modern/linked-signals/) β writable state that resets from a source
- [Testing Signals](/testing/testing-signals/)