Skip to main content

Angular Signals: A Practical Guide to Reactive State

December 29, 2025

by ODD4 Team

angular.ts
const count = signal(0);
const doubled = computed(() => count() * 2);

count.set(5);
console.log(doubled()); // 10

Angular Signals, introduced in version 16, represent a fundamental shift in how Angular handles reactivity. If you have been working with RxJS Observables and zone.js change detection, Signals offer a simpler, more predictable alternative for managing component state.

#What Are Signals?

A Signal is a wrapper around a value that notifies consumers when that value changes. Unlike Observables, Signals are synchronous and always have a current value.

import { signal } from '@angular/core';
 
// Create a signal with an initial value
const count = signal(0);
 
// Read the current value
console.log(count()); // 0
 
// Update the value
count.set(5);
console.log(count()); // 5

The key difference from traditional variables is that Angular tracks when you read a Signal and automatically updates the UI when that Signal changes.

#Why Signals Matter

#Fine-Grained Reactivity

With zone.js, Angular checks the entire component tree for changes. Signals enable fine-grained updates where only the specific DOM nodes that depend on a changed Signal get updated.

@Component({
  template: `
    <h1>{{ title() }}</h1>
    <p>Count: {{ count() }}</p>
    <button (click)="increment()">+1</button>
  `
})
export class CounterComponent {
  title = signal('My Counter');
  count = signal(0);
 
  increment() {
    this.count.update(c => c + 1);
    // Only the <p> element updates, not the <h1>
  }
}

#Simpler Mental Model

Observables require understanding subscriptions, operators, and memory management. Signals are straightforward: set a value, read a value.

// RxJS approach
private countSubject = new BehaviorSubject(0);
count$ = this.countSubject.asObservable();
 
increment() {
  this.countSubject.next(this.countSubject.value + 1);
}
 
// Signals approach
count = signal(0);
 
increment() {
  this.count.update(c => c + 1);
}

#Core Signal APIs

#signal() - Writable Signals

Create a mutable signal that you can read and write.

const name = signal('Alice');
 
// Read
name(); // 'Alice'
 
// Write (replace value)
name.set('Bob');
 
// Write (based on previous value)
name.update(current => current.toUpperCase());

#computed() - Derived Values

Create a read-only signal that automatically updates when its dependencies change.

import { signal, computed } from '@angular/core';
 
const firstName = signal('John');
const lastName = signal('Doe');
 
// Automatically updates when firstName or lastName changes
const fullName = computed(() => `${firstName()} ${lastName()}`);
 
console.log(fullName()); // 'John Doe'
firstName.set('Jane');
console.log(fullName()); // 'Jane Doe'

Computed signals are lazy. They only recalculate when you read them and a dependency has changed.

#effect() - Side Effects

Run code whenever a signal changes. Effects are useful for logging, syncing with localStorage, or making API calls.

import { signal, effect } from '@angular/core';
 
const theme = signal('light');
 
effect(() => {
  document.body.className = theme();
  localStorage.setItem('theme', theme());
});
 
// Changing the signal triggers the effect
theme.set('dark');

Effects run at least once during creation and then whenever any signal they read changes.

#Signals with Objects and Arrays

When working with objects or arrays, use the update method to maintain immutability.

interface User {
  name: string;
  age: number;
}
 
const user = signal<User>({ name: 'Alice', age: 30 });
 
// Update a property
user.update(current => ({ ...current, age: 31 }));
 
// Arrays
const items = signal<string[]>(['apple', 'banana']);
 
// Add item
items.update(current => [...current, 'cherry']);
 
// Remove item
items.update(current => current.filter(item => item !== 'banana'));

#Input Signals (Angular 17.1+)

Angular 17.1 introduced signal-based inputs, replacing the traditional @Input() decorator.

import { Component, input } from '@angular/core';
 
@Component({
  selector: 'app-greeting',
  template: `<h1>Hello, {{ name() }}!</h1>`
})
export class GreetingComponent {
  // Required input
  name = input.required<string>();
 
  // Optional input with default
  greeting = input('Hello');
}

Signal inputs are read-only and integrate seamlessly with computed signals.

export class UserCardComponent {
  firstName = input.required<string>();
  lastName = input.required<string>();
 
  // Derived from inputs
  fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
}

#When to Use Signals vs RxJS

Signals and RxJS serve different purposes. Here is a practical guide:

Use Signals for:

  • Component state (counters, form values, UI toggles)
  • Derived/computed values
  • Simple parent-child communication
  • Replacing BehaviorSubject for synchronous state

Use RxJS for:

  • HTTP requests and async operations
  • Complex event streams (debounce, throttle, merge)
  • WebSocket connections
  • When you need operators like switchMap, combineLatest, or retry

You can convert between them when needed:

import { toSignal, toObservable } from '@angular/core/rxjs-interop';
 
// Observable to Signal
const data = toSignal(this.http.get('/api/data'));
 
// Signal to Observable
const count = signal(0);
const count$ = toObservable(count);

#Migration Tips

If you are adding Signals to an existing project:

  1. Start with new components. Use Signals for any new features you build.

  2. Identify BehaviorSubject candidates. Simple synchronous state held in BehaviorSubjects often converts cleanly to Signals.

  3. Keep RxJS for async. Do not try to replace HTTP calls or complex streams with Signals.

  4. Use computed for derived state. Replace getter methods that combine multiple values with computed signals.

  5. Test incrementally. Signals work alongside zone.js, so you can migrate gradually.

#Performance Considerations

Signals improve performance in several ways:

  • Reduced change detection cycles. Only affected components update.
  • Lazy computation. Computed signals only recalculate when read.
  • No subscription management. Unlike Observables, you do not need to unsubscribe.

For most applications, simply using Signals will improve performance without any optimization effort. For advanced cases, Angular 18+ supports zoneless change detection with Signals.

#Summary

Angular Signals provide a simpler, more efficient approach to reactive state management. They reduce boilerplate, improve performance through fine-grained reactivity, and offer a gentler learning curve than RxJS for common use cases.

Start by using Signals for component state in new features, and gradually adopt them across your application as you become comfortable with the API.

Ready to get started?

Let's build something great together

Whether you need managed IT, security, cloud, or custom development, we're here to help. Reach out and let's talk about your technology needs.