What Are Signals?
Signals are a new reactive primitive in Angular that provide fine-grained reactivity. Unlike observables, signals are synchronous and automatically track dependencies.
Basic Signal Usage
import { signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
Count: {{ count() }}
Double: {{ doubleCount() }}
`
})
export class CounterComponent {
// Create a writable signal
count = signal(0);
// Create a computed signal (read-only, derived value)
doubleCount = computed(() => this.count() * 2);
increment() {
// Update signal value
this.count.update(value => value + 1);
// Or set directly: this.count.set(10);
}
}Effects
Effects run side effects when signals change:
@Component({...})
export class UserComponent {
userId = signal(null);
user = signal(null);
constructor() {
// Effect runs when userId changes
effect(async () => {
const id = this.userId();
if (id) {
const userData = await this.userService.getUser(id);
this.user.set(userData);
}
});
}
} Signal-Based Inputs
@Component({
selector: 'app-user-card',
template: `
{{ fullName() }}
{{ user().email }}
`
})
export class UserCardComponent {
// Signal-based input
user = input.required();
// Computed from input
fullName = computed(() =>
`${this.user().firstName} ${this.user().lastName}`
);
} Signal-Based Outputs
@Component({
selector: 'app-search',
template: `
`
})
export class SearchComponent {
query = signal('');
// Output using outputFromObservable or output()
searchChange = output();
onInput(event: Event) {
const value = (event.target as HTMLInputElement).value;
this.query.set(value);
this.searchChange.emit(value);
}
} State Management with Signals
// Simple store using signals
@Injectable({ providedIn: 'root' })
export class CartStore {
private items = signal([]);
// Public read-only access
readonly cartItems = this.items.asReadonly();
readonly totalItems = computed(() =>
this.items().reduce((sum, item) => sum + item.quantity, 0)
);
readonly totalPrice = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
addItem(product: Product) {
this.items.update(items => {
const existing = items.find(i => i.productId === product.id);
if (existing) {
return items.map(i =>
i.productId === product.id
? { ...i, quantity: i.quantity + 1 }
: i
);
}
return [...items, { productId: product.id, quantity: 1, price: product.price }];
});
}
removeItem(productId: string) {
this.items.update(items => items.filter(i => i.productId !== productId));
}
} Converting from RxJS
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
@Component({...})
export class DataComponent {
private http = inject(HttpClient);
// Convert Observable to Signal
users = toSignal(
this.http.get('/api/users'),
{ initialValue: [] }
);
selectedId = signal(null);
// Convert Signal to Observable
selectedId$ = toObservable(this.selectedId);
} Performance Benefits
- Fine-grained updates - only affected components re-render
- No zone.js dependency with zoneless change detection
- Synchronous reads - no async complexity
- Automatic dependency tracking
Best Practices
- Use signals for component state
- Use computed for derived values
- Use effects sparingly for side effects
- Keep signals close to where they're used
- Consider signal-based stores for shared state
Conclusion
Signals represent Angular's evolution toward simpler, more performant reactivity. Start using them in new components and gradually migrate existing code.