
Getting Started with Angular Signals
A beginner-friendly introduction to Angular Signals and how they simplify state management.
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 valueThere'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.