Tutorials Logic, IN info@tutorialslogic.com

Angular Signals Reactive State signal

Angular Signals Reactive State signal

Signals are Angular's fine-grained reactive state primitive, and they let templates update only when the exact signal value changes.

Focus on reads, writes, computed signals, and the way signal tracking narrows the amount of work Angular has to do.

A strong understanding of signals should include the difference between reading a signal, updating it, and deriving state from it.

Angular Signals Reactive State signal should be studied as a practical Angular lesson, not as a label. Start by naming the input, the rule that changes the input, and the result a learner should be able to predict after reading the page.

In the angular > signals page, the notes should connect the definition with a working scenario, a mistake that beginners actually make, and the exact check that proves the fix. That makes the topic useful for coding, debugging, and interview revision.

1. What are Signals?

A Signal is a wrapper around a value that notifies all consumers automatically whenever that value changes. Think of it as a reactive variable - when it updates, every part of the UI or logic that depends on it re-evaluates instantly.

Starting with Angular 16 (and made the default reactive primitive in Angular 21), Signals power Angular's new zoneless change detection. Instead of Zone.js patching every async operation and triggering full-tree checks, Angular now knows exactly which components need to re-render because Signals track their own dependencies.

Key characteristics of Signals:

  • Synchronous - reads and writes happen immediately, no async overhead.
  • Explicit - Angular knows precisely which signals a template or computed value depends on.
  • Fine-grained - only the components that actually read a changed signal are updated.
  • Zero extra libraries - built directly into @angular/core.

2. Why Signals? - The Problem They Solve

Before Signals, Angular relied on Zone.js for change detection. While Zone.js worked, it came with real costs:

Signals solve all of these. They are explicit (Angular tracks exactly what changed), synchronous (no async pipe needed), and require zero extra libraries beyond Angular itself.

Zone.js Problem Impact
Ran change detection on the entire component tree Slow in large apps
~100 KB bundle overhead Larger initial load
Monkey-patched browser APIs Hard to debug, unexpected behaviour
Required RxJS BehaviorSubject for simple state Boilerplate-heavy
async pipe needed in every template Verbose templates

3. signal() - Creating a Writable Signal

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);
    }
}

4. computed() - Derived Read-Only 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 changes (via the button), total automatically recalculates. The template re-renders only the parts that read total() - nothing else.

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);
    }
}

5. effect() - Running Side Effects

effect() registers a callback that runs whenever any signal it reads changes. It is ideal for synchronising state to the outside world - writing to localStorage, updating the DOM directly, logging analytics, or syncing to a third-party library.

Important rule: do not modify signals inside an effect. Doing so can create infinite loops. Effects are for reading and reacting, not for writing back.

The effect runs once immediately on creation, then again every time theme() changes. Angular automatically cleans up the effect when the component is destroyed.

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');
    }
}

6. Signals in Services - Shared 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);
}

7. input() - Signal-based Component Input

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" />

8. output() - Signal-based Output

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)" />

9. model() - Two-Way Binding Signal

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" />

10. toSignal() - Converting Observables to Signals

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')
    );
}

11. Signals vs RxJS - When to Use Each

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

12. Quick Reference - Signals API

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

Angular Signals Reactive State signal state check

Angular Signals Reactive State signal state check
const state = { topic: "Angular Signals Reactive State signal", ready: true };
if (state.ready) {
  console.log(state.topic + ": render or run the normal path");
}

Angular Signals Reactive State signal fallback check

Angular Signals Reactive State signal fallback check
const response = null;
const message = response?.message ?? "Angular Signals Reactive State signal: show a clear fallback";
console.log(message);
Key Takeaways
  • Explain the purpose of signals before memorizing syntax.
  • Run or trace one small Angular example and confirm the output.
  • Test one normal case, one edge case, and one mistake case for signals.
  • Write the rule in your own words after checking the example.
  • Connect signals to a real project scenario instead of treating it as an isolated definition.
Common Mistakes to Avoid
WRONG Memorizing Angular Signals Reactive State signal without the situation where it is useful.
RIGHT Connect Angular Signals Reactive State signal to a concrete Angular task.
Purpose makes syntax easier to recall.
WRONG Testing Angular Signals Reactive State signal only with the perfect input.
RIGHT Include empty, missing, duplicate, incompatible, or failed cases when relevant.
Real bugs usually appear outside the perfect path.
WRONG Changing code before reading the visible symptom or error message.
RIGHT Inspect the output, state, configuration, or stack trace connected to Angular Signals Reactive State signal.
Evidence keeps debugging focused.
WRONG Memorizing Angular Signals Reactive State signal without the situation where it is useful.
RIGHT Connect Angular Signals Reactive State signal to a concrete Angular task.
Purpose makes syntax easier to recall.

Practice Tasks

  • Modify the example so it handles a different input or condition.
  • Write one mistake related to signals, then fix it and explain the fix.
  • Summarize when to use signals and when another approach is better.
  • Write a small example that uses Angular Signals Reactive State signal in a realistic Angular scenario.
  • Change one important value in the Angular Signals Reactive State signal example and predict the result first.

Frequently Asked Questions

Angular Signals are reactive primitives introduced in Angular 16. A signal holds a value and notifies consumers when it changes. They provide a simpler alternative to RxJS for state management.

<code>signal()</code> creates writable reactive state that you update manually with .set() or .update(). <code>computed()</code> creates read-only derived state that automatically recalculates when its signal dependencies change.

Use <code>effect()</code> for side effects like logging, syncing to localStorage, or calling external APIs when signal values change. Avoid updating signals inside effects to prevent infinite loops.

Yes. Use <code>toSignal(observable$)</code> to convert an Observable to a Signal, and <code>toObservable(mySignal)</code> to convert a Signal to an Observable. Both are in @angular/core/rxjs-interop.

Zoneless Angular removes Zone.js dependency and uses Signals for change detection. Enable it with <code>provideExperimentalZonelessChangeDetection()</code>. It significantly improves performance and reduces bundle size.

Ready to Level Up Your Skills?

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