Angular Signals
Understand how Angular Signals create reactive state, derive values, track dependencies, and update the user interface efficiently.
Lesson content
What are Angular Signals? Angular Signals are reactive values that notify Angular when their data changes. Angular tracks where a signal is read and updates the related consumers when the signal value changes. Simple Definition A signal is a wrapper around a value that automatically notifies interested consumers when that value changes. Signal Flow Signal Value ↓ Template or Computed Reads Signal ↓ Angular Tracks Dependency ↓ Signal Value Changes ↓ Dependent Consumer Updates Why Use Signals? Manage local component state Automatically update templates Create derived values Share reactive state through services Reduce manual change-detection work Build fine-grained reactive applications 1. Create a Writable Signal Use the signal() function to create a writable signal. import { Component, signal } from '@angular/core'; @Component({ selector: 'app-counter', standalone: true, template: ` <p>Count: {{ count() }}</p> ` }) export class CounterComponent { count = signal(0); } A signal is read by calling it like a function. console.log(this.count()); 2. Update a Signal with set() Use set() when replacing the current value. count = signal(0); reset() { this.count.set(0); } Example name = signal('Ajay'); changeName() { this.name.set('Angular Developer'); } 3. Update a Signal with update() Use update() when the next value depends on the previous value. count = signal(0); increment() { this.count.update( currentValue => currentValue + 1 ); } Complete Counter Example import { Component, signal } from '@angular/core'; @Component({ selector: 'app-counter', standalone: true, template: ` <h2>{{ count() }}</h2> <button (click)="increment()"> Increment </button> <button (click)="decrement()"> Decrement </button> <button (click)="reset()"> Reset </button> ` }) export class CounterComponent { count = signal(0); increment() { this.count.update( value => value + 1 ); } decrement() { this.count.update( value => value - 1 ); } reset() { this.count.set(0); } } set() vs update() set() update() Replaces the current value Calculates a new value from the previous value Receives a direct value Receives an update function count.set(10) count.update(value => value + 1) 4. Signal with Objects interface User { id: number; name: string; } user = signal<User>({ id: 1, name: 'Ajay' }); Update an Object Signal changeName() { this.user.update( currentUser => ({ ...currentUser, name: 'Angular Developer' }) ); } Prefer immutable updates instead of directly changing object properties. 5. Signal with Arrays products = signal<string[]>([ 'Laptop', 'Keyboard' ]); Add an Item addProduct() { this.products.update( products => [ ...products, 'Mouse' ] ); } Remove an Item removeProduct(productName: string) { this.products.update( products => products.filter( product => product !== productName ) ); } 6. Computed Signals A computed signal derives its value from one or more other signals. Computed signals are read-only, lazy, and memoized. import { computed, signal } from '@angular/core'; quantity = signal(2); price = signal(500); total = computed(() => { return this.quantity() * this.price(); }); Template <p>Quantity: {{ quantity() }}</p> <p>Price: {{ price() }}</p> <p>Total: {{ total() }}</p> Computed Signal Flow quantity ──┐ ├──► computed total price ─────┘ ↓ Template Updates Computed Example with Filtering products = signal([ { id: 1, name: 'Laptop', active: true }, { id: 2, name: 'Mouse', active: false } ]); activeProducts = computed(() => this.products().filter( product => product.active ) ); 7. Writable vs Computed Signal Writable Signal Computed Signal Created with signal() Created with computed() Can use set() Cannot use set() Can use update() Read-only Stores source state Stores derived state 8. Read-Only Signals Use asReadonly() to expose a signal without allowing external code to call set() or update() . private countState = signal(0); readonly count = this.countState.asReadonly(); increment() { this.countState.update( value => value + 1 ); } A read-only signal prevents direct signal updates, but it does not prevent deep mutation of objects stored inside the signal. 9. Effects An effect runs side-effect code whenever the signals read inside it change. import { Component, effect, signal } from '@angular/core'; @Component({ selector: 'app-profile', standalone: true, template: ` <button (click)="changeName()"> Change Name </button> ` }) export class ProfileComponent { name = signal('Ajay'); constructor() { effect(() => { console.log( 'Name changed:', this.name() ); }); } changeName() { this.name.set( 'Angular Developer' ); } } Effect Flow Effect Runs ↓ Reads name() ↓ Angular Tracks name Signal ↓ name Changes ↓ Effect Runs Again Common Effect Use Cases Logging Synchronizing data with local storage Integrating third-party libraries Analytics Manual DOM or browser API synchronization Do Not Use Effects for Derived State Use computed() when one value can be calculated from other signals. Recommended: total = computed(() => this.price() * this.quantity() ); Avoid: effect(() => { this.total.set( this.price() * this.quantity() ); }); 10. linkedSignal() A linked signal is a writable signal whose default value is derived reactively from another source. It can be manually updated, but it automatically resets when its reactive source changes. import { linkedSignal, signal } from '@angular/core'; products = signal([ 'Laptop', 'Keyboard', 'Mouse' ]); selectedProduct = linkedSignal( () => this.products()[0] ); Manual Update selectProduct(product: string) { this.selectedProduct.set(product); } When the Source Changes replaceProducts() { this.products.set([ 'Monitor', 'Printer' ]); } The linked signal recalculates its value from the updated product list. When to Use linkedSignal() A selected item derived from a list A default option that users may change State that should reset when its source changes computed() vs linkedSignal() computed() linkedSignal() Read-only derived value Writable derived value Cannot be manually changed Can be changed with set() Recalculates from dependencies Resets when its reactive source changes 11. Signals in Templates Call the signal inside the template to read its current value. count = signal(0); <p>Count: {{ count() }}</p> Do not write: <p>Count: {{ count }}</p> 12. Signals with OnPush When an Angular template reads a signal, Angular tracks that signal as a dependency of the component. When the signal changes, Angular marks the affected component for an update. import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; @Component({ selector: 'app-counter', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: ` <p>{{ count() }}</p> ` }) export class CounterComponent { count = signal(0); } 13. Shared State with a Signal Service import { computed, Injectable, signal } from '@angular/core'; export interface CartItem { id: number; name: string; price: number; } @Injectable({ providedIn: 'root' }) export class CartService { private itemsState = signal<CartItem[]>([]); readonly items = this.itemsState.asReadonly(); readonly totalItems = computed(() => this.itemsState().length ); readonly totalPrice = computed(() => this.itemsState().reduce( (total, item) => total + item.price, 0 ) ); addItem(item: CartItem) { this.itemsState.update( items => [ ...items, item ] ); } removeItem(id: number) { this.itemsState.update( items => items.filter( item => item.id !== id ) ); } } Using the Signal Service import { Component, inject } from '@angular/core'; @Component({ selector: 'app-cart', standalone: true, template: ` <p> Items: {{ cart.totalItems() }} </p> <p> Total: {{ cart.totalPrice() }} </p> ` }) export class CartComponent { protected cart = inject(CartService); } 14. Signal Inputs The input() function creates a signal-based component input. import { Component, input } from '@angular/core'; @Component({ selector: 'app-user-card', standalone: true, template: ` <h2>{{ name() }}</h2> ` }) export class UserCardComponent { name = input('Guest'); } Required Input name = input.required<string>(); Parent Template <app-user-card [name]="username" /> 15. Signal Models The model() function creates a writable input that supports two-way binding. import { Component, model } from '@angular/core'; @Component({ selector: 'app-name-editor', standalone: true, template: ` <input [value]="value()" (input)="updateValue($event)" /> ` }) export class NameEditorComponent { value = model(''); updateValue(event: Event) { const inputElement = event.target as HTMLInputElement; this.value.set( inputElement.value ); } } Parent Template <app-name-editor [(value)]="username" /> 16. Resource A resource manages asynchronous data and exposes its value, loading status, error, and state through signals. Use a resource primarily for asynchronous read operations. import { resource, signal } from '@angular/core'; userId = signal(1); userResource = resource({ params: () => ({ id: this.userId() }), loader: async ({ params }) => { const response = await fetch( `/api/users/${params.id}` ); if (!response.ok) { throw new Error( 'Unable to load user' ); } return response.json(); } }); Read Resource Signals userResource.value() userResource.isLoading() userResource.error() userResource.status() Template Example @if (userResource.isLoading()) { <p>Loading user...</p> } @if (userResource.error()) { <p>Unable to load user</p> } @if (userResource.hasValue()) { <h2> {{ userResource.value().name }} </h2> } 17. httpResource() httpResource() is a reactive wrapper around Angular's HTTP client. It exposes the HTTP response, loading state, error, and status as signals. import { Component, input } from '@angular/core'; import { httpResource } from '@angular/common/http'; @Component({ selector: 'app-user', standalone: true, template: ` @if (user.isLoading()) { <p>Loading...</p> } @if (user.hasValue()) { <h2>{{ user.value().name }}</h2> } ` }) export class UserComponent { userId = input.required<number>(); user = httpResource<User>( () => `/api/users/${this.userId()}` ); } 18. Dynamic Dependencies Computed signals and effects track only the signals that were read during their latest execution. showCount = signal(false); count = signal(0); message = computed(() => { if (!this.showCount()) { return 'Count is hidden'; } return `Count: ${this.count()}`; }); When showCount() is false, the computed signal does not depend on count() . 19. untracked() Use untracked() when a signal value must be read without creating a reactive dependency. import { effect, signal, untracked } from '@angular/core'; currentUser = signal('Ajay'); counter = signal(0); constructor() { effect(() => { const user = this.currentUser(); const currentCount = untracked( () => this.counter() ); console.log( user, currentCount ); }); } This effect reruns when currentUser changes, but not when counter changes. 20. Signal Equality Signals use Object.is() equality by default. A custom equality function can prevent unnecessary updates when two values should be considered equivalent. const user = signal( { id: 1, name: 'Ajay' }, { equal: ( previous, current ) => previous.id === current.id && previous.name === current.name } ); Signals vs Normal Variables Normal Variable Signal Stores a value Stores a reactive value Read directly Read by calling it count count() Changed with assignment Changed with set() or update() No dependency tracking Angular tracks consumers Signals vs RxJS Observables Signals Observables Represent current reactive state Represent values over time Synchronous value access Values arrive through subscriptions Read using signal() Read using subscription or async pipe Useful for UI and local state Useful for events and complex async streams No manual subscription for template reads Subscriptions may require cleanup Signals do not completely replace RxJS. They solve different problems and can be used together. RxJS Interoperability Convert Observable to Signal import { toSignal } from '@angular/core/rxjs-interop'; users = toSignal( this.userService.getUsers(), { initialValue: [] } ); Convert Signal to Observable import { toObservable } from '@angular/core/rxjs-interop'; searchTerm = signal(''); searchTerm$ = toObservable(this.searchTerm); Common Mistakes 1. Forgetting Parentheses Wrong: {{ count }} Correct: {{ count() }} 2. Direct Object Mutation Avoid: this.user().name = 'Rahul'; Prefer: this.user.update( user => ({ ...user, name: 'Rahul' }) ); 3. Using effect() for Derived Values Avoid: effect(() => { this.total.set( this.price() * this.quantity() ); }); Prefer: total = computed(() => this.price() * this.quantity() ); 4. Exposing Writable State Publicly Avoid: items = signal<CartItem[]>([]); Prefer: private itemsState = signal<CartItem[]>([]); readonly items = this.itemsState.asReadonly(); Best Practices Use signal() for source state Use computed() for derived state Use linkedSignal() for writable derived state Use effect() only for genuine side effects Prefer immutable updates for arrays and objects Expose shared service state as read-only signals Keep writable signals private when possible Use resource() and httpResource() for appropriate asynchronous read flows Continue using RxJS for complex asynchronous streams and event composition Avoid unnecessary signal conversions Interview Questions What is an Angular Signal? A signal is a reactive wrapper around a value that notifies interested consumers when its value changes. How do you read a signal? Call the signal like a function. count() What is the difference between set() and update()? set() replaces the value directly, while update() calculates the next value from the previous value. What is a computed signal? A computed signal is a read-only, memoized signal whose value is derived from other signals. What is an effect? An effect is a reactive operation that runs when the signals read inside it change. It should mainly be used for side effects. What is linkedSignal()? A linked signal is writable state whose default value is reactively derived from another source. Do Signals replace RxJS? No. Signals are well suited to reactive state and templates, while RxJS remains useful for asynchronous streams, events, cancellation, and complex operator-based workflows. What is a resource? A resource manages asynchronous read data and exposes its value, loading state, error, and status through signals. Key Point Signals provide fine-grained reactive state that Angular can track and use to update only the consumers that depend on the changed value. Easy Formula signal() = Writable State set() = Replace Value update() = Change from Previous Value computed() = Read-Only Derived State linkedSignal() = Writable Derived State effect() = Side Effect resource() = Async Reactive Data asReadonly() = Protected Public State State Changes ↓ Signal Notifies Consumers ↓ Computed Values Recalculate ↓ Angular Updates Related UI