Tutorials Logic, IN info@tutorialslogic.com

Angular Signals Reactive State signal

Signal Foundations

Angular signals track synchronous state dependencies precisely. Learn writable signals, computed derivation, effects, component input and model APIs, service state, and RxJS interop without turning every value into global mutable state.

Signal Definition

A signal is a synchronous getter that publishes a value and records reactive consumers when they read it. A writable signal adds set and update operations; computed, input, model, and query signals expose specialized read behavior.

When a template reads a signal, Angular records that dependency and schedules the consuming view when the value changes. Computed values and effects also track only the signals read during their latest execution, so dependencies can change when branches change.

Use signals for owned state and derivation, not as a wrapper around every constant or local variable:

  • Synchronous - reads and writes happen immediately, no async overhead.
  • Explicit - Angular knows precisely which signals a template or computed value depends on.
  • Tracked - templates, computed values, and effects register the signals they actually read.
  • Equality-aware - a write notifies consumers only when the configured equality rule considers the value changed.

Dependency Tracking and Equality

Signal dependencies are dynamic. A computed callback that reads premiumRate only while isPremium is true stops depending on premiumRate when that branch is skipped. This prevents unrelated writes from invalidating the computed value.

Signals use referential equality by default. Replace objects and arrays instead of mutating them in place. A custom equality function can suppress equivalent writes, but it also adds comparison cost and can hide meaningful updates if its contract is too broad.

Concern Practical rule
Conditional dependency Only signals read on the active branch are tracked
Object or array update Return a new value from update instead of mutating the current value
Incidental read Use untracked when a read must not become a dependency
Derived value Use computed instead of synchronizing writable signals with an effect
Equivalent replacement Use a custom equal function only after defining and testing equivalence

Writable Signals

signal(initialValue) creates a writable signal. You read it by calling it like a function - count(). You write to it with .set() for a direct value or .update() for a function that receives the current value.

The three operations you need to know:

Operation Syntax When to use
Read count() In templates or computed/effect bodies
Set count.set(5) Replace with a known value
Update count.update(v => v + 1) Derive next value from current

Counter with signal()

Counter with signal()
import { Component, signal } from '@angular/core';

@Component({
    selector: 'app-counter',
    standalone: true,
    template: `
        <p>Count: {{ count() }}</p>
        <button (click)="increment()">+1</button>
        <button (click)="reset()">Reset</button>
    `
})
export class CounterComponent {
    count = signal(0);

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

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

Derived State and Effects

Computed Signals

computed() creates a read-only signal whose value is derived from one or more other signals. It is lazy - it only recalculates when one of its signal dependencies actually changes, and it caches the result in between. You cannot call .set() or .update() on a computed signal.

Every time itemCount or price changes, total becomes stale and recalculates on its next read. Angular marks views that consume total; normal change detection then updates any DOM bindings whose values changed.

Cart total with computed()

Cart total with computed()
import { Component, signal, computed } from '@angular/core';

@Component({
    selector: 'app-cart',
    standalone: true,
    template: `
        <p>Items: {{ itemCount() }}</p>
        <p>Price: ${{ price() }}</p>
        <p><b>Total: ${{ total() }}</b></p>
        <button (click)="addItem()">Add Item</button>
    `
})
export class CartComponent {
    itemCount = signal(1);
    price     = signal(9.99);

    total = computed(() => this.itemCount() * this.price());

    addItem() {
        this.itemCount.update(n => n + 1);
    }
}

Effects

effect registers a side effect that reruns when a signal read by its latest execution changes. Use it to synchronize reactive state with an external system such as storage, analytics, a canvas, or a non-Angular widget.

Do not use an effect to calculate state that computed or linkedSignal can express. Writing state from an effect can create circular updates and extra render passes; when an external synchronization genuinely writes a signal, make the direction and termination rule explicit.

An effect needs an injection context unless an Injector is supplied. Effects created by a component or service are destroyed with that owner; use manual cleanup only when a deliberately longer lifetime is required.

Theme sync with effect()

Theme sync with effect()
import { Component, signal, effect } from '@angular/core';

@Component({
    selector: 'app-theme',
    standalone: true,
    template: `
        <button (click)="toggleTheme()">
            Theme: {{ theme() }}
        </button>
    `
})
export class ThemeComponent {
    theme = signal('light');

    constructor() {
        effect(() => {
            localStorage.setItem('theme', this.theme());
            document.body.setAttribute('data-theme', this.theme());
        });
    }

    toggleTheme() {
        this.theme.update(t => t === 'light' ? 'dark' : 'light');
    }
}

untracked and Custom Equality

Use untracked to read a signal inside reactive code without adding it as a dependency. This is useful for incidental context such as including the current counter in a log that should rerun only when the selected user changes.

A custom equal function changes the definition of a meaningful update. Keep it deterministic and test it with the same values the UI cares about; deep comparison on large structures can cost more than the recomputation it avoids.

  • Do not use untracked to hide a dependency that should update the result.
  • Referential equality works well with immutable object and array updates.
  • Equality controls notification, not whether set or update can be called.

Read Incidental Context without Tracking It

Read Incidental Context without Tracking It
const selectedUser = signal<User | null>(null);
const requestCount = signal(0);

effect(() => {
  const user = selectedUser();
  const count = untracked(requestCount);

  analytics.record('selection', { userId: user?.id, count });
});

// Changing requestCount alone does not rerun this effect.

Component and Shared State

Shared Signal State

The most powerful pattern is placing signals inside an Injectable service. The service becomes the single source of truth for a piece of state. Any component that injects the service can read the signals directly in its template - no BehaviorSubject, no async pipe, no subscriptions to manage.

Expose the internal signal as .asReadonly() so consumers cannot accidentally mutate it. Only the service's own methods can change the state.

Multiple components can inject CounterService and they all share the same signal state. When one component calls svc.increment(), every other component reading svc.count() updates automatically.

Signals in a Service

Signals in a Service
import { Injectable, signal, computed } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class CounterService {
    private _count = signal(0);

    readonly count   = this._count.asReadonly();
    readonly doubled = computed(() => this._count() * 2);
    readonly isZero  = computed(() => this._count() === 0);

    increment() { this._count.update(v => v + 1); }
    decrement() { this._count.update(v => v - 1); }
    reset()     { this._count.set(0); }
}

6. Signals in Services - Shared State

6. Signals in Services - Shared State
import { Component, inject } from '@angular/core';
import { CounterService } from './counter.service';

@Component({
    selector: 'app-counter',
    standalone: true,
    template: `
        <h2>Count: {{ svc.count() }}</h2>
        <p>Doubled: {{ svc.doubled() }}</p>
        <button (click)="svc.increment()">+</button>
        <button (click)="svc.decrement()">-</button>
        <button (click)="svc.reset()">Reset</button>
    `
})
export class CounterComponent {
    svc = inject(CounterService);
}

Signal Inputs

input() is the modern replacement for @Input(). The value is a read-only signal, so you can use it directly inside computed() and effect() without any extra wiring. Use input.required<T>() to mark an input as mandatory - Angular will throw a compile-time error if the parent does not provide it.

Because name and prefix are signals, message recomputes automatically whenever the parent changes either binding - no ngOnChanges needed.

Signal-based input()

Signal-based input()
import { Component, input, computed } from '@angular/core';

@Component({
    selector: 'app-greeting',
    standalone: true,
    template: `<h2>{{ message() }}</h2>`
})
export class GreetingComponent {
    name   = input.required<string>();
    prefix = input('Hello');

    message = computed(() => `${this.prefix()}, ${this.name()}!`);
}
// Usage: <app-greeting name="Angular" prefix="Welcome to" />

Signal Outputs

output() replaces @Output() + EventEmitter with a simpler, more explicit API. You call .emit(value) to fire the event, and the parent listens with the same (eventName)="handler($event)" syntax it always used.

Signal-based output()

Signal-based output()
import { Component, output } from '@angular/core';

@Component({
    selector: 'app-like-button',
    standalone: true,
    template: `<button (click)="like()">Like ({{ likeCount }})</button>`
})
export class LikeButtonComponent {
    likeCount = 0;
    liked = output<number>();

    like() {
        this.likeCount++;
        this.liked.emit(this.likeCount);
    }
}
// Usage: <app-like-button (liked)="onLiked($event)" />

Model Signals

model() combines input() and output() into a single two-way bindable signal. It is the signal-based equivalent of [(ngModel)] for custom components. The parent binds with [(propertyName)] and the child can both read and write the value.

When the child calls this.checked.update(), Angular automatically emits a checkedChange event, which the two-way binding syntax [()] uses to update the parent's variable.

Two-way binding with model()

Two-way binding with model()
import { Component, model } from '@angular/core';

@Component({
    selector: 'app-toggle',
    standalone: true,
    template: `<button (click)="toggle()">{{ checked() ? 'ON' : 'OFF' }}</button>`
})
export class ToggleComponent {
    checked = model(false);

    toggle() {
        this.checked.update(v => !v);
    }
}
// Usage: <app-toggle [(checked)]="isActive" />

Signal Queries

viewChild and viewChildren expose elements, directives, or child components from the component view as signals. contentChild and contentChildren do the same for projected content. The query value updates when conditional content appears, disappears, or changes.

Use required queries only when the template contract guarantees a match. Read ElementRef only when no Angular binding or directive API can express the interaction, and avoid coupling business logic to raw DOM structure.

  • A query does not pierce another component template.
  • Prefer querying a directive or component type over a CSS-dependent raw element.
  • Use afterNextRender when DOM measurement must wait until rendering completes.

Required View Query

Required View Query
@Component({
  selector: 'app-search-box',
  template: '<input #queryInput type="search">'
})
export class SearchBox {
  private readonly queryInput =
    viewChild.required<ElementRef<HTMLInputElement>>('queryInput');

  focus(): void {
    this.queryInput().nativeElement.focus();
  }
}

Linked State and Async Resources

Use linkedSignal for writable state whose valid default depends on another signal. It resets when its source changes but can still be changed by the user between source updates. This fits a selected item that must remain valid when the available options are replaced.

Use resource for asynchronous data driven by signal parameters when the resource loading contract fits the feature. It exposes status, value, error, reload, and cancellation through an AbortSignal. Use httpResource for reactive HTTP reads; keep writes in explicit HttpClient commands.

  • computed is read-only derivation; linkedSignal is writable state linked to a source.
  • A resource loader should honor the supplied AbortSignal so stale requests can stop.
  • Handle idle, loading, resolved, error, and reloading states explicitly in the template.
  • Do not use a resource for mutation commands whose retries or side effects require explicit control.

Keep a Selection Valid with linkedSignal

Keep a Selection Valid with linkedSignal
const shippingOptions = signal<readonly ShippingOption[]>([]);

const selectedOption = linkedSignal({
  source: shippingOptions,
  computation: (options, previous) =>
    options.find(option => option.id === previous?.value.id)
      ?? options[0]
      ?? null
});

selectedOption.set(expressOption); // user choice until options change

Load Signal-driven Data with resource

Load Signal-driven Data with resource
const userId = input.required<string>();

const userResource = resource({
  params: () => ({ id: userId() }),
  loader: async ({ params, abortSignal }) => {
    const response = await fetch('/api/users/' + params.id, {
      signal: abortSignal
    });

    if (!response.ok) throw new Error('HTTP ' + response.status);
    return await response.json() as User;
  }
});

RxJS Interoperability

Observable Interop

toSignal() from @angular/core/rxjs-interop bridges the RxJS world and the Signals world. It wraps an Observable and returns a Signal - no subscribe(), no async pipe, no manual unsubscribe(). Angular manages the subscription lifecycle automatically.

The signal starts as undefined until the HTTP response arrives, which is why the template checks @if (users()) before iterating. You can also pass { initialValue: [] } as a second argument to avoid the undefined state.

HTTP data via toSignal()

HTTP data via toSignal()
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';

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

    users = toSignal(
        this.http.get('https://jsonplaceholder.typicode.com/users')
    );
}

Signals or RxJS

Signals and RxJS are complementary, not competing. Use the right tool for the job:

A practical rule of thumb: start with Signals for all synchronous state. Reach for RxJS when you need operators like debounceTime, switchMap, or retry, then bridge back to Signals with toSignal() for the template.

Scenario Signals RxJS
Component state (counter, toggle, form field)
Derived / computed values
HTTP requests via toSignal()
WebSocket / real-time streams
Complex async pipelines (debounce, retry, switchMap)
Shared app state (user session, cart, theme)
Template binding without async pipe

Signal Reference

API Import Description
signal(value) @angular/core Creates a writable signal
computed(() => ...) @angular/core Derived read-only signal, lazy & cached
effect(() => ...) @angular/core Side effect that re-runs on signal change
input() @angular/core Signal-based @Input replacement
input.required() @angular/core Required signal input (compile-time enforced)
output() @angular/core Signal-based @Output replacement
model() @angular/core Two-way bindable signal (input + output)
viewChild() @angular/core Signal-based @ViewChild
contentChild() @angular/core Signal-based @ContentChild
toSignal(obs$) @angular/core/rxjs-interop Converts an Observable to a Signal
toObservable(sig) @angular/core/rxjs-interop Converts a Signal to an Observable
linkedSignal(...) @angular/core Writable state whose default follows another signal
resource(...) @angular/core Signal-driven asynchronous loading with status and cancellation
httpResource(...) @angular/common/http Reactive HTTP read driven by signal inputs
untracked(...) @angular/core Reads a signal without recording a reactive dependency
Confirm the page outcome

Signal Review

5 checks
  • I can read, set, and update writable signals without mutating object or array values in place.
  • I use computed for derived values, linkedSignal for writable linked state, and effect only for external synchronization.
  • I can explain dynamic dependencies, untracked reads, and the consequences of a custom equality function.
  • I expose service state as readonly signals and keep mutations behind named methods.
  • I can choose between resource, httpResource, RxJS, toSignal, and toObservable based on cancellation and operator needs.

Signal Review Questions

Angular tracks only the signals read during the latest computed execution. If a branch reads discount() only when membership() is true, discount is not a dependency while that branch is skipped. That is intentional dynamic dependency tracking.

Direct mutation bypasses the signal setter. Use user.update(current => ({ ...current, name })) so Angular receives a new value and notifies consumers. Arrays have the same issue: return a new array instead of pushing into the existing one.

Usually no. Use computed for read-only derivation and linkedSignal for writable state that resets from a source. Reserve effect for synchronization with an external system, because signal-to-signal writes can create circular updates and unnecessary render passes.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.