Back to Blog
Angular Signals: Reactive State Made Easy
angularsignalsfrontendreactive-stateperformance-engineering

Angular Signals: Reactive State Made Easy

Angular Signals make reactive state easier to reason about. A breakdown of writable, computed, linked, and effect-based primitives for precise dependency tracking.

Angular Signals: Reactive State Made Easy

Angular Signals make reactive state easier to reason about.

I created a slide deck, a visual diagram, and a Google Sheet while researching Angular Signals — Angular's reactive model built for precise dependency tracking and performant UI updates.

What Are Signals?

Signals are Angular's reactive primitive. They hold a value and automatically notify consumers when that value changes. Unlike observables, signals are synchronous and always have a current value.

Writable Signals

The most basic signal type. You create it with signal() and update it with set() or update():

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

@Component({
  selector: 'app-counter',
  template: `
    <h2>Count: {{ count() }}</h2>
    <button (click)="increment()">+1</button>
    <button (click)="decrement()">-1</button>
    <button (click)="reset()">Reset</button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterComponent {
  count = signal(0);

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

  decrement() {
    this.count.update((value) => value - 1);
  }

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

Important: Do NOT use mutate on signals. Always use update or set instead to keep state transformations pure and predictable.

Computed Signals

Derived state that automatically updates when its dependencies change:

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

@Component({
  selector: 'app-shopping-cart',
  template: `
    <h2>Cart ({{ itemCount() }} items)</h2>
    <p>Subtotal: {{ subtotal() | currency }}</p>
    <p>Tax: {{ tax() | currency }}</p>
    <p><strong>Total: {{ total() | currency }}</strong></p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShoppingCartComponent {
  items = signal<CartItem[]>([
    { name: 'Widget', price: 25, quantity: 2 },
    { name: 'Gadget', price: 50, quantity: 1 },
  ]);

  itemCount = computed(() =>
    this.items().reduce((sum, item) => sum + item.quantity, 0)
  );

  subtotal = computed(() =>
    this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
  );

  tax = computed(() => this.subtotal() * 0.08);

  total = computed(() => this.subtotal() + this.tax());
}

Linked Signals

linkedSignal creates a writable signal that automatically resets when a source signal changes, but can also be manually overridden:

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

@Component({
  selector: 'app-notification-settings',
  template: `
    <h3>Theme: {{ selectedTheme() }}</h3>
    <select (change)="onThemeChange($event)">
      @for (theme of themes(); track theme) {
        <option [value]="theme">{{ theme }}</option>
      }
    </select>

    <label>
      <input
        type="checkbox"
        [checked]="notifications()"
        (change)="toggleNotifications()"
      />
      Enable notifications
    </label>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NotificationSettingsComponent {
  selectedTheme = signal('light');
  themes = signal(['light', 'dark', 'auto']);

  // Linked to selectedTheme — resets when theme changes,
  // but can be toggled manually
  notifications = linkedSignal(() => this.selectedTheme() === 'light');

  onThemeChange(event: Event) {
    const target = event.target as HTMLSelectElement;
    this.selectedTheme.set(target.value);
  }

  toggleNotifications() {
    this.notifications.update((v) => !v);
  }
}

Effects

Effects run side effects whenever their signal dependencies change. Use them sparingly — for logging, persistence, or syncing with external systems:

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

@Component({
  selector: 'app-search',
  template: `
    <input
      type="text"
      [value]="query()"
      (input)="onInput($event)"
      placeholder="Search..."
    />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SearchComponent {
  query = signal('');

  constructor() {
    // Effect automatically tracks `query` signal
    effect(() => {
      const currentQuery = this.query();
      if (currentQuery.length > 2) {
        console.log('Searching for:', currentQuery);
        // Trigger API call or debounced search
      }
    });
  }

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

Signal-Based Inputs and Outputs

The modern Angular API uses signals for component communication:

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

@Component({
  selector: 'app-user-card',
  template: `
    <div class="card" (click)="cardClicked.emit(user())">
      <h3>{{ fullName() }}</h3>
      <p>{{ user().email }}</p>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
  user = input.required<User>();
  cardClicked = output<User>();

  fullName = computed(
    () => `${this.user().firstName} ${this.user().lastName}`
  );
}

Resource API for Async Data

The resource() API bridges signals with async data fetching:

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

@Component({
  selector: 'app-user-detail',
  template: `
    @if (userResource.isLoading()) {
      <p>Loading...</p>
    } @else if (userResource.error()) {
      <p>Error loading user</p>
    } @else {
      <h2>{{ userResource.value()?.name }}</h2>
      <p>{{ userResource.value()?.email }}</p>
    }

    <button (click)="nextUser()">Next User</button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserDetailComponent {
  userId = signal(1);

  userResource = resource({
    request: () => ({ id: this.userId() }),
    loader: async ({ request }) => {
      const res = await fetch(`https://api.example.com/users/${request.id}`);
      return res.json();
    },
  });

  nextUser() {
    this.userId.update((id) => id + 1);
  }
}

Signals introduce writable, computed, linked, and effect-based primitives that work together to create predictable, scalable state flows — even when async data is involved.

Resources are attached to the original LinkedIn post 👇 Curious how others are using signals in production Angular apps.

Related Posts

Angular Dependency Injection: A Visual Deep Dive

Angular Dependency Injection: A Visual Deep Dive

A comprehensive slide-deck breakdown of Angular DI — from @Injectable to InjectionTokens, injector hierarchy, and advanced resolution strategies like skipSelf and host.

angulartypescriptfrontend+2 more
Read More
Angular DI Essentials: What Changed the Game for Me

Angular DI Essentials: What Changed the Game for Me

Most Angular devs use Dependency Injection every day without fully understanding it. Here are the key concepts that levelled up my Angular architecture.

angulartypescriptfrontend+2 more
Read More
Mastering Angular Components: The Building Blocks

Mastering Angular Components: The Building Blocks

A deep dive into Angular components — from structure and composition to lifecycle hooks, change detection, and DOM interactions.

angularfrontendui-architecture+2 more
Read More

Design & Developed by Sachin Singh
© 2026. All rights reserved.