Signals are the biggest change to Angular's reactivity model since the framework launched. They let you manage component state without subscriptions, without async pipe, and without manually calling markForCheck(). Here's how they actually work.

The Problem Signals Solve

Before signals, reactive component state in Angular typically meant RxJS BehaviorSubject:

// Before signals — BehaviorSubject pattern
export class CartComponent {
  private cartCount$ = new BehaviorSubject(0)
  count$ = this.cartCount$.asObservable()

  add() {
    this.cartCount$.next(this.cartCount$.getValue() + 1)
  }
}
<!-- template needed async pipe -->
<span>{{ count$ | async }}</span>

With signals:

// With signals
export class CartComponent {
  count = signal(0)

  add() {
    this.count.update(c => c + 1)
  }
}
<!-- template reads signal directly -->
<span>{{ count() }}</span>

No BehaviorSubject, no async pipe, no subscription management. The signal value is read by calling it like a function.

signal() — Creating Reactive State

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

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <div class="card border-0 shadow-sm p-4 text-center">
      <h2 class="display-4 fw-bold mb-3" style="color:#fd4766;">{{ count() }}</h2>
      <div class="d-flex gap-2 justify-content-center">
        <button class="btn btn-outline-secondary" (click)="decrement()">−</button>
        <button class="btn text-white" style="background:#fd4766;" (click)="increment()">+</button>
        <button class="btn btn-outline-secondary" (click)="reset()">Reset</button>
      </div>
    </div>
  `
})
export class CounterComponent {
  count = signal(0)

  increment() { this.count.update(c => c + 1) }
  decrement() { this.count.update(c => c - 1) }
  reset()     { this.count.set(0) }
}

Three ways to update a signal:

// set() — replace value directly
this.count.set(10)

// update() — derive next value from current
this.count.update(current => current + 1)

// mutate() — for objects/arrays, mutate in place
this.items.mutate(arr => arr.push(newItem))

computed() — Derived State

computed() creates a read-only signal that automatically recalculates when its dependencies change:

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

@Component({
  selector: 'app-cart',
  standalone: true,
  template: `
    <div class="card border-0 shadow-sm p-4">
      <div class="d-flex justify-content-between mb-3">
        <span class="fw-bold">Items</span>
        <span class="fw-bold" style="color:#fd4766;">{{ total() | currency }}</span>
      </div>

      <div class="d-flex align-items-center gap-3 mb-3">
        <label class="form-label mb-0 small">Qty:</label>
        <input type="number" class="form-control form-control-sm w-auto"
          [value]="quantity()"
          (change)="quantity.set(+$any($event.target).value)">
      </div>

      <p class="text-muted small mb-0">
        {{ quantity() }} × {{ unitPrice() | currency }} = {{ total() | currency }}
      </p>
      <p class="text-muted small mb-0" *ngIf="hasDiscount()">
        🎉 Discount applied: {{ discountedTotal() | currency }}
      </p>
    </div>
  `
})
export class CartComponent {
  quantity  = signal(1)
  unitPrice = signal(29.99)

  // Recalculates whenever quantity or unitPrice changes
  total = computed(() => this.quantity() * this.unitPrice())

  // Chains computed signals
  hasDiscount    = computed(() => this.quantity() >= 3)
  discountedTotal = computed(() => this.hasDiscount() ? this.total() * 0.9 : this.total())
}

computed() is lazy — it only recalculates when actually read, and only if a dependency changed. Angular caches the value between reads.

effect() — Side Effects

effect() runs whenever the signals it reads change:

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

@Component({
  selector: 'app-theme',
  standalone: true,
  template: `
    <div class="d-flex align-items-center gap-3">
      <span class="small fw-semibold">Theme:</span>
      <button class="btn btn-sm"
        [class.btn-dark]="isDark()"
        [class.btn-outline-secondary]="!isDark()"
        (click)="isDark.update(d => !d)">
        {{ isDark() ? '🌙 Dark' : '☀️ Light' }}
      </button>
    </div>
  `
})
export class ThemeComponent {
  isDark = signal(false)

  constructor() {
    // Runs immediately and whenever isDark changes
    effect(() => {
      document.documentElement.setAttribute(
        'data-bs-theme',
        this.isDark() ? 'dark' : 'light'
      )
      localStorage.setItem('theme', this.isDark() ? 'dark' : 'light')
    })
  }
}

effect() must be called in an injection context (constructor or field initializer). Don't write signals inside effects without allowSignalWrites: true — use update() in event handlers instead.

input() — Signal-Based Inputs

Angular 17.1+ introduced input() as a signal-based replacement for @Input():

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

@Component({
  selector: 'app-template-card',
  standalone: true,
  template: `
    <div class="card border-0 shadow-sm h-100">
      <div class="card-body p-4">
        <div class="d-flex justify-content-between align-items-start mb-2">
          <h5 class="fw-bold mb-0">{{ name() }}</h5>
          <span class="badge text-white" style="background:#fd4766;">
            {{ framework() }}
          </span>
        </div>
        <p class="text-muted small mb-3">{{ description() }}</p>
        <div class="d-flex justify-content-between align-items-center">
          <span class="fw-bold" style="color:#fd4766;">{{ displayPrice() }}</span>
          <a [href]="'https://lettstartdesign.com'"
            class="btn btn-sm text-white" style="background:#fd4766;"
            target="_blank" rel="noopener">
            Buy — FIRST30
          </a>
        </div>
      </div>
    </div>
  `
})
export class TemplateCardComponent {
  // Signal inputs — automatically reactive
  name        = input.required<string>()
  description = input.required<string>()
  framework   = input<string>('Bootstrap')
  price       = input<number>(29)

  // Computed from inputs — updates when inputs change
  displayPrice = computed(() => `$${this.price()}`)
}
<!-- Usage -->
<app-template-card
  name="Marvel Dashboard"
  description="Angular 21 + Bootstrap 5 admin template"
  framework="Angular"
  [price]="49"
/>

Signals vs RxJS — When to Use Which

ScenarioUse
Component state (counter, toggle, selected item)signal()
Derived state from other statecomputed()
DOM side effects (theme, scroll position)effect()
HTTP requestsRxJS + HttpClient
WebSocket / EventSource streamsRxJS
Debounce user input before API callRxJS debounceTime
Multiple async streams combinedRxJS combineLatest
Router eventsRxJS

The practical rule: if it's local state that lives in a component, use signals. If it involves time, async, or stream operators, use RxJS. Most Angular 21 apps use both side by side.

toSignal() — Bridging RxJS to Signals

Angular provides toSignal() to convert an observable into a signal, so you can use RxJS for HTTP and then read the result as a signal in templates:

import { Component, inject } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { toSignal } from '@angular/core/rxjs-interop'

@Component({
  selector: 'app-templates',
  standalone: true,
  template: `
    @if (templates()) {
      @for (t of templates(); track t.id) {
        <div>{{ t.name }}</div>
      }
    } @else {
      <p>Loading...</p>
    }
  `
})
export class TemplatesComponent {
  private http = inject(HttpClient)

  // Observable converted to signal — no async pipe needed
  templates = toSignal(
    this.http.get<any[]>('/api/templates')
  )
}

No async pipe, no subscription, no ngOnDestroy. toSignal() handles cleanup automatically.

Frequently Asked Questions

Signals are a reactive primitive in Angular (introduced in v16, stable in v17+) that hold a value and notify consumers when that value changes. They're simpler than RxJS observables for local component state — no subscriptions, no pipe chains, no unsubscribe logic.
Not entirely. Signals are best for synchronous component state — counter, toggle, form value, selected tab. RxJS is still the right tool for async operations — HTTP requests, WebSockets, complex event streams, debounce/throttle. Most Angular 21 apps use both.
A computed signal derives its value from other signals automatically. When the source signals change, the computed value updates too, and only consumers that depend on it re-evaluate. computed(() => price() * quantity()) updates whenever price or quantity changes.
effect() runs a side effect whenever the signals it reads change. It's Angular's equivalent of React's useEffect for signal-derived side effects — logging, localStorage sync, third-party library updates. Avoid heavy logic inside effects; prefer computed() for derived state.

Need a Full Bootstrap 5 Admin Dashboard?

Get a complete Angular 21 + Bootstrap 5 dashboard with 50+ components — built by the same team behind BootstrapPlanet.

Browse Templates →

Use code FIRST30 for 30% off your first purchase.

Related Components