Skip to main content

Signals

About 6 min

Signals

Signals in Angular are a new reactivity model that let components automatically respond to state changes without relying on Zone.js or manual change detection.

A signal is a special reactive variable that stores a value and notifies any dependent computations or templates when it changes. When you update a signal, Angular knows exactly which parts of the UI depend on it and updates only those parts — making change detection faster, more predictable, and easier to reason about.

They're used to:

  • Replace @Input bindings, RxJS, or manual ChangeDetectorRef patterns for local reactive state.
  • Improve performance by running fine-grained reactivity instead of full component tree checks.
  • Simplify code, since templates automatically re-render when a signal's value changes.

Difference between signals and regular variables

Regular variables in Angular are not reactive — when you change them, Angular has no built-in way to know that something in the template depends on them. The UI only updates after Angular's change detection cycle runs (for example, after a template event, async operation, or a manual trigger).

Signals, on the other hand, are reactive primitives. When you read a signal in a template or a computed function, Angular automatically tracks that dependency. When the signal's value changes, Angular marks only the affected components or expressions for update — no full tree check or manual notification needed.

In short:

  • Regular variables: Passive data; UI updates only when change detection happens for other reasons.
  • Signals: Active, tracked state; directly trigger minimal, targeted updates when they change.

This makes signals both more predictable and more efficient than relying on traditional change detection alone.

An example

Only regular variables

  • Create an Angular 20+ zoneless app and create a new counter-component
import { Component } from "@angular/core";

@Component({
  selector: "app-counter-component",
  imports: [],
  templateUrl: "./counter-component.html",
  styleUrl: "./counter-component.css",
})
export class CounterComponent {
  countV = 0;

  doubleV = this.countV * 2;

  incV() {
    this.countV = this.countV + 1;
  }
  decV() {
    this.countV = this.countV - 1;
  }
  resetV() {
    this.countV = 0;
  }
}
<div class="card">
  <h3>Counter regular variable</h3>
  <p>countV: <strong>{{ countV }}</strong></p>
  <p>doubleV: <strong>{{ doubleV }}</strong></p>

  <button (click)="incV()">+1</button>
  <button (click)="decV()">-1</button>
  <button (click)="resetV()">Reset</button>
</div>
.card {
  border: 1px solid #ddd;
  padding: 1rem;
  border-radius: 0.75rem;
}
<!-- app.html -->
<h1>Signals & Change Detection</h1>
<section>
  <h2>Counter</h2>
  <app-counter-component></app-counter-component>
</section>
  • Clicking +1 triggers change detection (template event): counterV in template is updated
  • Clicking -1 triggers change detection (template event): counterV in template is updated
  • doubleV stays 0, no change detection triggered
  • Clicking Reset triggers change detection (template event): counterV in template is updated

With signals

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

@Component({
  selector: "app-counter-component",
  imports: [],
  templateUrl: "./counter-component.html",
  styleUrl: "./counter-component.css",
})
export class CounterComponent {
  countV = 0;
  countS = signal(0);

  doubleV = this.countV * 2;
  doubleS = computed(() => this.countS() * 2);

  incV() {
    this.countV = this.countV + 1;
  }
  decV() {
    this.countV = this.countV - 1;
  }
  resetV() {
    this.countV = 0;
  }

  incS() {
    this.countS.update((n) => n + 1);
  }
  decS() {
    this.countS.update((n) => n - 1);
  }
  resetS() {
    this.countS.set(0);
  }
}
<div class="card">
  <h3>Counter regular variable</h3>
  <p>countV: <strong>{{ countV }}</strong></p>
  <p>doubleV: <strong>{{ doubleV }}</strong></p>

  <button (click)="incV()">+1</button>
  <button (click)="decV()">-1</button>
  <button (click)="resetV()">Reset</button>
</div>

<div class="card">
  <h3>Counter signal</h3>
  <p>countS(): <strong>{{ countS() }}</strong></p>
  <p>doubleS(): <strong>{{ doubleS() }}</strong></p>

  <button (click)="incS()">+1</button>
  <button (click)="decS()">-1</button>
  <button (click)="resetS()">Reset</button>
</div>
  • Clicking +1 triggers change detection (update on signal): counterS in template is updated
  • Clicking -1 triggers change detection (update on signal): counterS in template is updated
  • doubleS in template is updated whenever counterS is updated because it is a computed signal
  • Clicking Reset triggers change detection (set on signal and computed signal): counterS and doubleS in template are updated

Signals and computed values

A signal is created with the signal() function — it holds a reactive value that automatically updates the UI when changed. This is a writable signal!

A computed is a derived signal whose value depends on other signals and recalculates automatically when they change.

In our example:

  • countS() reads the signal's current value.
  • countS.set() or countS.update() changes it.
  • computed() automatically recalculates when its dependencies change.

Computed signals are not writable signals

doubleS = computed(() => this.countS() * 2); // OK
doubleS.set(24); // NOT OK = compilation error

Effects

Signals are useful because they notify interested consumers when they change. An effect is an operation that runs whenever one or more signal values change. You can create an effect with the effect function:

// side-effect that reacts to signal changes
log = effect(() => {
  // This will run on init, and whenever `count` changes
  console.log("[effect] count is", this.countS());
});
  • Effects always run at least once
  • An effect tracks any signal value. If any of these signal values change, the effect runs again.
  • Effects always execute asynchronously, during the change detection process.

Use cases for effects

Effects are rarely needed in most application code, but may be useful in specific circumstances. Here are some examples of situations where an effect might be a good solution:

  • Logging data being displayed and when it changes, either for analytics or as a debugging tool.
  • Keeping data in sync with window.localStorage.
  • Adding custom DOM behavior that can't be expressed with template syntax.
  • Performing custom rendering to a <canvas>, charting library, or other third party UI library.

When not to use effects

Avoid using effects for propagation of state changes. This can result in ExpressionChangedAfterItHasBeenChecked errors, infinite circular updates, or unnecessary change detection cycles.

Instead, use computed or linked signals to model state that depends on other state.

LinkedSignals

A linked signal is a hybrid reactive primitive: it combines the reactivity of a computed() signal (i.e. it automatically recalculates when its dependencies change) with the writability of a normal signal(). It lets you express derived state that can also be overridden or locally mutated.

So in architecture terms: when you have a piece of component/local state that depends on some upstream state (e.g., input props, store signals, fetched data) but also needs to allow local user modifications — that's a strong candidate for linkedSignal.

Common use cases for linked signals are:

  • Reset/initialisation semantics: If upstream changes (e.g., the selected item changed), you want to reset a dependent field to a default value. But if the user had already modified that field and the upstream still supports it, you might want to preserve the user's value.
  • Multiple source signals: You might have a dependent piece of state that depends on two or more upstream signals (for example filter criteria from dropdown + text input). Using linkedSignal you can derive from several signals elegantly.
  • Writable derived state: Sometimes you want a derived value but also allow it to be overridden by user. For example a “percentage” derived from two signals but user may also edit the percentage manually. A computed() alone wouldn’t allow write.

Signal-based inputs

Angular 20 introduces a new way to define component inputs using Signals — a more reactive and fine-grained mechanism than traditional @Input() bindings.

Traditional Inputs

In traditional Angular, inputs are defined using the @Input() decorator:

@Component({
  selector: "child-cmp",
  template: `<p>Value: {{ value }}</p>`,
})
export class ChildCmp {
  @Input() value!: number;
}

And passed from the parent component like this:

@Component({
  selector: "parent-cmp",
  template: `<child-cmp [value]="counter"></child-cmp>`,
})
export class ParentCmp {
  counter = 1;
}

Problems with this approach:

  • ngOnChanges() is needed to detect input changes
  • Derived values require manual recomputation

A click event will trigger change detection, but an aysnchronous change of the value (like a timer, Observable, ...) won't!

import { Component } from "@angular/core";
import { CounterComponent } from "./counter-component/counter-component";
import { ScoreComponent } from "./score-component/score-component";
import { ProductSelectionComponent } from "./product-selection-component/product-selection-component";
import { ChildComponent } from "./child-component/child-component";
import { interval, tap } from "rxjs";

@Component({
  selector: "app-root",
  imports: [
    CounterComponent,
    ScoreComponent,
    ProductSelectionComponent,
    ChildComponent,
  ],
  templateUrl: "./app.html",
  styleUrl: "./app.css",
})
export class App {
  traditionalInputCounter = 1;

  constructor() {
    interval(1000)
      .pipe(
        tap(() => {
          // asynchronous change of data won't trigger change detection
          console.log(this.traditionalInputCounter);
          this.incrementTraditionalInputCounter();
        })
      )
      .subscribe();
  }

  // button click will trigger change detection
  incrementTraditionalInputCounter() {
    this.traditionalInputCounter += 1;
  }
}

Signal-based inputs

Angular's Signals API provides a new reactive foundation for components. A signal is a reactive variable that automatically updates any expressions that depend on it. In Angular 20+, you can now define signal-based inputs using the input() function directly.

@Component({
  selector: "child-cmp",
  template: `
    <p>Value: {{ value() }}</p>
    <p>Double: {{ double() }}</p>
  `,
})
export class ChildCmp {
  value = input<number>(); // Signal input
  double = computed(() => this.value() * 2);

  constructor() {
    effect(() => console.log("Value changed:", this.value()));
  }
}
@Component({
  selector: "parent-cmp",
  template: `
    <child-cmp [value]="counter()"></child-cmp>
    <button (click)="increment()">Increment</button>
  `,
})
export class ParentCmp {
  counter = signal(1);
  increment() {
    this.counter.update((v) => v + 1);
  }
}
import { Component, signal } from "@angular/core";
import { CounterComponent } from "./counter-component/counter-component";
import { ScoreComponent } from "./score-component/score-component";
import { ProductSelectionComponent } from "./product-selection-component/product-selection-component";
import { ChildComponent } from "./child-component/child-component";
import { interval, tap } from "rxjs";

@Component({
  selector: "app-root",
  imports: [
    CounterComponent,
    ScoreComponent,
    ProductSelectionComponent,
    ChildComponent,
  ],
  templateUrl: "./app.html",
  styleUrl: "./app.css",
})
export class App {
  singalInputCounter = signal(1);

  constructor() {
    interval(1000)
      .pipe(
        tap(() => {
          this.singalInputCounter.update((n) => n + 1);
        })
      )
      .subscribe();
  }
}

Each time the signal gets a new value, change detection is triggered for the component. That's why the UI rerenders every second.

httpResource

The Angular team introduced a new experimental API called httpResource, built on top of the core resource primitive and the standard HttpClient.

Key motivations:

  • Provide a declarative, signal-based approach to fetching HTTP data: you no longer need to manually subscribe, manage loading/error/value states, or cancel obsolete requests.
  • Leverage HttpClient underneath, so you still benefit from interceptors, angular testing facilities, existing HTTP infrastructure.
  • Simplify boilerplate and integrate more seamlessly with Angular's Signals and reactive model.

httpResource exposes its result through Signals.

Caution

It is marked experimental in Angular 20. The team cautions that the API may change before it becomes stable.

In the following example you can see what we can access (as signals) from the httpResource:

// Service
export class ProductService {
  private productsUrl = "api/products";

  // Retrieve data into a signal
  productsResource = httpResource<Product[]>(() => this.productsUrl, {
    defaultValue: [],
  });
}





 
 
 

// Component
@Component({
  selector: "app-http-resource-component",
  imports: [],
  templateUrl: "./http-resource-component.html",
  styleUrl: "./http-resource-component.css",
})
export class HttpResourceComponent {
  private productService = inject(ProductService);
  productsResource = this.productService.productsResource;
}









 

<!-- Template -->
<h2>httpResource</h2>

@if (productsResource.isLoading()) {
<p>Loading products...</p>
} @else if (productsResource.error()) {
<p>Error: {{ productsResource.error()?.message }}</p>
} @else {
<ul>
  @for (product of productsResource.value(); track $index) {
  <li>{{ product.productName }}</li>
  }
</ul>
}



 

 
 


 




  • isLoading(): boolean signal indicating whether this resource is loading a new value (or reloading the existing one).
  • error(): Error signal, when in the error state, this returns the last known error from the Resource
  • error()?.message: The actual error message when in error state
  • value(): The current value of the Resource

Remember: these are all signals: when the value changes, the template will update!