
Quick answer: An Angular signal is a reactive container for a value. Read it by calling it, write it with set or update, and derive new values with computed. Angular re-renders only the parts that read a changed signal, so signals are both simpler and faster than manual change tracking. For most apps a service full of signals is enough state management; reach for NgRx when many features share complex state.The sections below cover the three Angular signals primitives, component state, sharing signals through a service, and the point where Angular signals alone are not enough and you reach for a store.
Table of Contents
The three core Angular signals tools
import { signal, computed, effect } from '@angular/core';
const count = signal(0); // writable
const doubled = computed(() => count() * 2); // derived, read-only
count.set(5); // replace the value
count.update(c => c + 1); // update from the previous value
effect(() => console.log('count is', count())); // runs when it changesRead a signal by calling it: count(), never count. computed creates a value that recalculates automatically and caches until a dependency changes. effect runs side effects such as logging or syncing to localStorage, and Angular’s guidance is to use it sparingly — derived state should be a computed, not an effect.
Component state with Angular signals
In a component, signals replace a lot of manual wiring. Because the template reads the signal, Angular knows exactly which DOM to update.
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-todos',
template: `
<input #box (keyup.enter)="add(box.value); box.value = ''" />
<p>{{ remaining() }} of {{ todos().length }} left</p>
@for (todo of todos(); track todo.id) {
<label><input type="checkbox" [checked]="todo.done"
(change)="toggle(todo.id)" /> {{ todo.title }}</label>
}
`,
})
export class TodosComponent {
todos = signal<{ id: number; title: string; done: boolean }[]>([]);
remaining = computed(() => this.todos().filter(t => !t.done).length);
add(title: string) {
if (!title.trim()) return;
this.todos.update(list => [...list, { id: Date.now(), title, done: false }]);
}
toggle(id: number) {
this.todos.update(list =>
list.map(t => (t.id === id ?
{ ...t, done: !t.done } : t)));
}
}
Notice the immutability: update returns a new array rather than mutating the old one. Signals compare by reference, so creating a new array is what tells Angular the list changed.
Signals in a service: state you can share
Move the same signals into a root service and you have shared application state with no library. Every component that injects the service sees the same values and updates automatically.
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class AuthService {
private current = signal<User | null>(null);
readonly user = this.current.asReadonly();
readonly isLoggedIn = computed(() => this.current() !== null);
login(user: User) { this.current.set(user); }
logout() { this.current.set(null); }
}Expose asReadonly() so consumers can watch the value but cannot change it from outside the service. That single habit keeps state changes auditable.
When you need NgRx
Signals in services cover the large majority of apps. NgRx earns its place when state is genuinely complex: many features read and write the same entities, you need a recorded sequence of every action, or several teams contribute to one state tree. The current NgRx is version 22 and offers two styles.
| Approach | Shape | Best for |
|---|---|---|
| Service with signals | A few signals and computed values | Most apps; simplest to learn |
| NgRx SignalStore | Signal-native store with methods | Feature state that several components share |
| Classic NgRx Store | Actions, reducers, effects, selectors | Large apps needing a full event log and time travel |
Start with signals in a service. If you find yourself writing the same update logic in several places, or needing to reason about state over time, that is the signal to introduce NgRx — not before.
Next: testing and debugging Angular apps. To learn signals and state on a real project, Ampersand Academy teaches Angular one-to-one.
Frequently asked questions
What is a signal in Angular?
An Angular signal is a reactive container for a value. You read it by calling it and write it with set or update, and Angular re-renders the parts of the view that read it when it changes.
When should I use computed instead of effect?
Use computed for values derived from other signals, because it caches and recalculates automatically. Use effect only for side effects such as logging or syncing to an external API.
Do I need NgRx with Angular signals?
No. For most applications, signals in a shared service are enough state management. NgRx is worth adding for large apps with complex shared state or a need to audit every change.
Why does my signal update not refresh the view?
You probably mutated the value instead of replacing it. Signals compare by reference, so call set or update with a new array or object to trigger a re-render.
What is asReadonly on a signal?
It exposes a read-only view of a writable signal, so components can watch the value but only the service that owns the signal can change it.
Last updated on · Written by Dinesh Kumar R

