Angular Signals Have Changed Angular Forever — Here's the Complete Guide

Published: (March 29, 2026 at 04:11 PM EDT)
3 min read
Source: Dev.to

Source: Dev.to

What Are Angular Signals?

Signals are reactive primitives that notify Angular when their value changes. Instead of Zone.js checking everything, Angular only updates what actually changed.

Creating Signals

import { Component, signal, computed, effect } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    
Count: {{ count() }}

    
Doubled: {{ doubled() }}

    +
    Reset
  `
})
export class CounterComponent {
  count = signal(0);
  doubled = computed(() => this.count() * 2);

  constructor() {
    effect(() => {
      console.log('Count changed:', this.count());
    });
  }

  increment() {
    this.count.update(c => c + 1);
  }

  reset() {
    this.count.set(0);
  }
}

Signal API

// Create
const name = signal('Alice');
const user = signal({ name: 'Alice', age: 30 });
const items = signal([]);

// Read
console.log(name()); // 'Alice'

// Set
name.set('Bob');

// Update (based on previous value)
items.update(prev => [...prev, 'new item']);

// Computed (derived)
const greeting = computed(() => `Hello, ${name()}!`);

// Effect (side effects)
effect(() => {
  document.title = `User: ${name()}`;
});

Signal-Based Inputs

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-user-card',
  template: `
    
      
## {{ name() }}

      
Age: {{ age() }}

      
{{ email() }}

    
  `
})
export class UserCardComponent {
  name = input.required();
  age = input.required();
  email = input(); // optional
}

Signal-Based Outputs

import { Component, output } from '@angular/core';

@Component({
  selector: 'app-search',
  template: ``
})
export class SearchComponent {
  searchChange = output();

  onInput(event: Event) {
    const value = (event.target as HTMLInputElement).value;
    this.searchChange.emit(value);
  }
}

Signal Store (NgRx SignalStore)

import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';

export const TodoStore = signalStore(
  withState({
    todos: [] as Todo[],
    loading: false,
    filter: 'all' as 'all' | 'active' | 'completed',
  }),
  withComputed((store) => ({
    filteredTodos: computed(() => {
      const todos = store.todos();
      const filter = store.filter();
      switch (filter) {
        case 'active': return todos.filter(t => !t.completed);
        case 'completed': return todos.filter(t => t.completed);
        default: return todos;
      }
    }),
    completedCount: computed(() => 
      store.todos().filter(t => t.completed).length
    ),
  })),
  withMethods((store) => ({
    addTodo(title: string) {
      patchState(store, (state) => ({
        todos: [...state.todos, { id: Date.now(), title, completed: false }],
      }));
    },
    toggleTodo(id: number) {
      patchState(store, (state) => ({
        todos: state.todos.map(t => 
          t.id === id ? { ...t, completed: !t.completed } : t
        ),
      }));
    },
  }))
);

Before and After

// Before (Zone.js era)
@Component({ ... })
export class OldComponent implements OnInit {
  count = 0;
  doubled = 0;
  ngOnInit() { this.doubled = this.count * 2; }
  increment() { this.count++; this.doubled = this.count * 2; }
}

// After (Signals)
@Component({ ... })
export class NewComponent {
  count = signal(0);
  doubled = computed(() => this.count() * 2); // auto-updates!
  increment() { this.count.update(c => c + 1); }
}

Building Angular dashboards with real data? Check out my Apify actors — structured data for any Angular app. For custom solutions, email spinov001@gmail.com.

0 views
Back to Blog

Related posts

Read more »