Getting Started with Angular Signals
0
Technology

Getting Started with Angular Signals

A beginner-friendly introduction to Angular Signals and how they simplify state management.

📅 Published Mar 20, 2026 🔄 Updated Jun 4, 2026 ⏱️9 min read👁54 views
Read in:

Angular Signals: Everything You Need to Know (With Real Examples)

If you've been working with Angular for a while, you probably know the issues of tracking down why your component re-rendered three times for one small change or why your ChangeDetectionStrategy.OnPush component is still updated when you didn't expect it to. Angular Signals came in to fix exactly that, and honestly, once you get the hang of them, going back feels weird.

This article covers everything: what signals are, why they exist, how they work, the different types, and real code examples you can actually use.

What Is a Signal in Angular?

A signal is a wrapper around a value. But what makes it special is that Angular knows automatically when that value changes. You don't have to tell Angular "hey, something changed, go check". The signal does that work for you.

Before signals, Angular had to run change detection across the whole component tree from top to bottom, or you had to be very careful with OnPush and markForCheck(). Signals remove that guesswork. When a signal's value changes, only the parts of the UI that actually read that signal get updated.

Angular introduced signals officially in Angular 16 as a developer preview, and they became stable in Angular 17.

Why Were Signals Added?

Angular has always used Zone.js to detect changes. Zone.js patches browser APIs (like setTimeout, Promise, events) so Angular knows when async things happen and can trigger change detection.

The problems with that approach:

  • Zone.js is a big library added to your bundle
  • It can't know which data changed; it just knows something might have changed
  • So Angular checks everything, which gets slow in large apps
  • Debugging why something re-rendered is hard

Signals solve this by making reactivity explicit. When you create a signal and use it in a template, Angular sets up a direct link: "this signal feeds this part of the UI." When the signal updates, Angular updates only that part. No guessing, no full tree traversal.

Creating the First Angular Signal

import { signal } from '@angular/core';
const count = signal(0);

You just created a signal with an initial value of 0. Now:

  • To read the value: count(): you call it like a function
  • To set a new value: count.set(5)
  • To update based on the current value: count.update(val => val + 1)

Simple example in a component:

import { Component, signal } from '@angular/core';
@Component({
  selector: 'app-counter',
  template: `
    <p>Count: {{ count() }}</p>
    <button (click)="increment()">Add</button>
    <button (click)="reset()">Reset</button>
  `})
export class CounterComponent {
  count = signal(0);
  increment() {
    this.count.update(val => val + 1);
  }
  reset() {
    this.count.set(0);
  }
}

No ngOnInit, no subscriptions, no async pipe, no markForCheck. It just works.

Types of Signals in Angular

1. Writable Signal

This is what signal() creates. You can read and write to it.

const name = signal('John');
name.set('Jane');           // set a new value directly
name.update(n => n + '!'); // update based on current value

There's also mutate() for objects and arrays, but it was removed in Angular 17. Now you use set() or update() with a new reference instead.

2. Computed Signal

A computed signal derives its value from one or more other signals. It recalculates only when its dependencies change.

import { signal, computed } from '@angular/core';=
const firstName = signal('John');
const lastName = signal('Doe');
const fullName = computed(() => `${firstName()} ${lastName()}`);
console.log(fullName()); // "John Doe"
firstName.set('Jane');
console.log(fullName()); // "Jane Doe"

Key points about computed:

  • It's lazy: it doesn't calculate until you read it
  • It caches the result: if dependencies haven't changed, it returns the cached value
  • You cannot write to it: it's read-only
  • It automatically tracks which signals it reads inside

3. Effect

An effect runs a side effect whenever signals inside it change. Think of it like useEffect in React, but it automatically knows what to watch.

import { signal, effect } from '@angular/core';
const count = signal(0);
effect(() => {
  console.log('Count changed to:', count());
});
count.set(1); // logs: "Count changed to: 1"
count.set(2); // logs: "Count changed to: 2"

Effects are created inside an injection context inside a constructor, ngOnInit, or using runInInjectionContext. You can't just drop them anywhere.

@Component({ ... })
export class MyComponent {
  count = signal(0);
  constructor() {
    effect(() => {
      console.log('Count is now:', this.count());
    });
  }
}

When to use effects:

  • Syncing signal values to localStorage
  • Logging or analytics tracking
  • Updating something that's outside Angular's template system

When NOT to use effects:

  • Don't use effects to sync one signal's value to another signal. Use computed for that.
  • Avoid putting business logic in effects that could live in a service or computed.

4. Signal Inputs (Angular 17.1+)

Angular 17.1 added signal-based inputs. Instead of the traditional @Input() decorator, you can now write:

import { Component, input } from '@angular/core';
@Component({
  selector: 'app-user-card',
  template: `<p>{{ username() }}</p>`
})
export class UserCardComponent {
  username = input<string>('Guest'); // default value is 'Guest'
}

You can also make it required:

username = input.required<string>();

The parent passes it the same way as before:

<app-user-card [username]="currentUser" />

Why is this better? Because username() is now a signal, you can use it in computed and effect, and Angular's change detection knows exactly when it changed.

5. Signal Outputs (Angular 17.3+)

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

@Component({ ... })
export class ButtonComponent {
  clicked = output<string>();
  handleClick() {
    this.clicked.emit('button was clicked');
  }
}

And in the parent:

<app-button (clicked)="onButtonClick($event)" />

6. Model Inputs (Two-Way Binding with Signals)

Angular 17.2 added model() for two-way binding between parent and child components.

// child component
import { Component, model } from '@angular/core';
@Component({
  selector: 'app-toggle',
  template: `
    <button (click)="toggle()">{{ isOn() ? 'ON' : 'OFF' }}</button>
  `})
export class ToggleComponent {
  isOn = model(false);
  toggle() {
    this.isOn.update(v => !v);
  }
}
<!-- parent template -->
<app-toggle [(isOn)]="myToggleState" />

When the child updates isOn, the parent's myToggleState also updates. Clean two-way binding without any extra boilerplate.

Signal vs Observable: When to Use What

This is something a lot of people ask. Here's a straightforward answer:

Situation

Use

Local component state

Signal

Derived values from the state

Computed signal

Side effects based on the state

Effect

HTTP requests

Observable (with HttpClient)

Complex async streams

Observable (RxJS)

Component inputs

Signal input

Simple value passing

Signal

Signals are for synchronous, local state. Observables are for async streams and events. They're not enemies-you'll often use both in the same app.

You can even convert between them:

import { toSignal, toObservable } from '@angular/core/rxjs-interop';
// Observable to Signal
const data$ = this.http.get('/api/data');
const dataSignal = toSignal(data$, { initialValue: null });
// Signal to Observable
const count = signal(0);
const count$ = toObservable(count);

Real-Time Example: Shopping Cart

Here's a more complete example using signals for a simple shopping cart:

import { Component, signal, computed } from '@angular/core';
interface CartItem {
  id: number;
  name: string;
  price: number;
  qty: number;
}
@Component({
  selector: 'app-cart',
  template: `
    <div *ngFor="let item of cartItems()">
      <span>{{ item.name }}</span>
      <button (click)="decrease(item.id)">-</button>
      <span>{{ item.qty }}</span>
      <button (click)="increase(item.id)">+</button>
      <span>₹{{ item.price * item.qty }}</span>
    </div>
    <hr />
    <strong>Total: ₹{{ total() }}</strong>
    <p>Items in cart: {{ itemCount() }}</p>
  `})
export class CartComponent {
  cartItems = signal<CartItem[]>([
    { id: 1, name: 'Shirt', price: 499, qty: 1 },
    { id: 2, name: 'Shoes', price: 1299, qty: 1 },
  ]);
  total = computed(() =>
    this.cartItems().reduce((sum, item) => sum + item.price * item.qty, 0)
  );
  itemCount = computed(() =>
    this.cartItems().reduce((sum, item) => sum + item.qty, 0)
  );
  increase(id: number) {
    this.cartItems.update(items =>
      items.map(item => item.id === id ? { ...item, qty: item.qty + 1 } : item)
    );
  }
  decrease(id: number) {
    this.cartItems.update(items =>
      items.map(item =>
        item.id === id && item.qty > 1 ? { ...item, qty: item.qty - 1 } : item
      )
    );
  }
}

The total and itemCount computed signals automatically update whenever cartItems changes. You don't write any update logic for them.

Using Signals in Service Class

Signals are not only for components. You can use them in services to share state across your app.

import { Injectable, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class AuthService {
  private user = signal<{ name: string; role: string } | null>(null);
  isLoggedIn = computed(() => this.user() !== null);
  userName = computed(() => this.user()?.name ?? 'Guest');
  isAdmin = computed(() => this.user()?.role === 'admin');
  login(name: string, role: string) {
    this.user.set({ name, role });
  }
  logout() {
    this.user.set(null);
  }
}
Now in any component:
@Component({
  template: `
    <span *ngIf="auth.isLoggedIn()">Welcome, {{ auth.userName() }}!</span>
    <button *ngIf="!auth.isLoggedIn()" (click)="login()">Log In</button>
  `})
export class NavbarComponent {
  auth = inject(AuthService);
  login() {
    this.auth.login('John', 'user');
  }
}

Effect Cleanup

If an effect sets up a subscription or a timer, you can return a cleanup function:

effect((onCleanup) => {
  const timer = setInterval(() => {
    console.log('tick', this.count());
  }, 1000);

  onCleanup(() => clearInterval(timer));
});

Angular calls the cleanup before re-running the effect and when the component is destroyed.

Common Mistakes to Avoid

1. Writing to a signal inside a computed

// wrong
const doubled = computed(() => {
  someOtherSignal.set(count() * 2); // don't do this
  return count() * 2;
});

Computed signals should be pure read-only, with no side effects.

2. Creating effects outside the injection context

// wrong — in a regular method
someMethod() {
  effect(() => { ... }); // will throw an error
}
// correct — in constructor
constructor() {
  effect(() => { ... });
}

3. Over-using effects

If you find yourself writing an effect(() => { this.derivedValue.set(...) }), stop and use computed instead.

4. Not using update() for arrays/objects

// wrong
const items = signal([1, 2, 3]);
items().push(4); // Angular won't know this changed
// correct
items.update(arr => [...arr, 4]);

Signal-Based Components (The Future)

Angular is moving toward zoneless applications where Zone.js is not needed at all. Signals are the foundation for that. When you use signals throughout your app, Angular can use a much smarter change detection system that only checks what actually changed.

You can try zoneless today:

// main.ts
bootstrapApplication(AppComponent, {
  providers: [
    provideExperimentalZonelessChangeDetection()
  ]
});

And remove zone.js from your polyfills in angular.json. It's still experimental, but it works well for apps that are fully signal-based.

Quick Reference

// Create a writable signal
const name = signal('Angular');
// Read it
name();
// Set a new value
name.set('Signals');
// Update based on current value
name.update(v => v + '!');
// Computed (derived, read-only)
const upper = computed(() => name().toUpperCase());
// Effect (side effects)
effect(() => console.log(name()));
// Signal input (Angular 17.1+)
username = input<string>('default');
username = input.required<string>();
// Signal output (Angular 17.3+)
clicked = output<void>();
// Two-way binding (Angular 17.2+)
value = model(0);
// Convert Observable to Signal
const sig = toSignal(observable$, { initialValue: null });
// Convert Signal to Observable
const obs$ = toObservable(signal);

Summary

Signals are not a replacement for everything in Angular; they work best for state management inside components and services. For HTTP and complex async work, Observables still make more sense. The sweet spot is using both where they fit naturally.

The biggest shift in thinking is this: instead of telling Angular when to check for changes, signals tell Angular what to check. That's a much better model, and it's where Angular is heading. Starting to use signals in new code now is a good call; it lines up with the direction the framework is taking.

Start small: replace a BehaviorSubject in a service with a signal. Move one component's state to signals. See how it feels. Most developers who try it don't go back.

📂 Categories

🏷️ Tags

About the author

Software Developer

0Blogs
0Followers

Discussion

AnonymousGuest