They basically help to manage reactive states—but they serve slightly different purposes.
What is a Signal in Angular?
A signal is a reactive primitive introduced in Angular 16+ to track state changes in a more declarative and efficient way — without requiring Observables or ChangeDetectorRef
.
Writable Signal (signal()
)
It’s the most common type and can be updated directly.
import { signal } from '@angular/core';
const count = signal(0);
// Get value
console.log(count()); // 0
// Set a new value
count.set(1);
// Update with function
count.update(current => current + 1);
Use it when:
- You need to read and write state.
- You're managing local component state, like counters, form states, etc.
Read-only Signal (computed()
or readonly()
)
This is a derived signal, typically created using computed()
or readonly()
.
It is not writable directly.
import { signal, computed } from '@angular/core';
const count = signal(2);
const double = computed(() => count() * 2);
console.log(double()); // 4
// double.set(10); ❌ Not allowed (read-only)
If you forcefully convert a writable signal to read-only, you use:
import { readonly } from '@angular/core';
const state = signal('draft');
const readOnlyState = readonly(state);
// readOnlyState.set('published'); ❌ Not allowed
Use it when:
- You want derived values or read-only state.
- You want to expose state safely without allowing outside updates.
Summary Table
Feature | signal() (Writable Signal) | computed() / readonly() (Read-only Signal) |
Can update value | ✅ Yes | ❌ No (derived or locked) |
Can be used for state | ✅ Yes | ✅ As a derived state |
Used for input value | ✅ Often | No direct input |
Mutation-safe | ⚠️ Needs care | ✅ Safe from mutation |
Ideal for | State you read/write | Derived state / exposed state without updates |
Example in Angular Component
@Component({
selector: 'my-counter',
standalone: true,
template: `
<button (click)="increment()">Count: {{ count() }}</button>
<p>Double: {{ doubleCount() }}</p>
`
})
export class CounterComponent {
count = signal(0); // Writable signal
doubleCount = computed(() => this.count() * 2); // Read-only signal
increment() {
this.count.update(c => c + 1);
}
}