Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 

Repository files navigation

Angular Signals Reactive State

A complete, from-scratch learning guide to Angular Signals, built around one evolving application: the Employee Management Portal for an IT company with branches in Pune and Chennai.

You will not find scattered, unrelated snippets in this guide. Every chapter adds one more capability to the same application, the same way a real Angular codebase grows over time. By the time you reach the last chapter, you will have designed, in your head and on paper, a fully signal-driven Angular application — inputs, outputs, computed values, effects, async data fetching, forms, and enterprise-grade folder structure.

This README is written to be read before you write a single line of code. Read it top to bottom once. Then come back to individual chapters as reference while you build.


Table of Contents

  1. Overview
  2. Why Angular Signals?
  3. Problems with Traditional State Management
  4. Evolution: Zone.js → Change Detection → RxJS → Signals
  5. Learning Roadmap
  6. Project Overview
  7. Folder Structure
  8. Creating the Angular Project
  9. Understanding Reactivity
  10. Signals Fundamentals
  11. Computed Signals
  12. Effects
  13. Writable Signals
  14. Input Signals
  15. Output Signals
  16. Linked Signals
  17. Resource API
  18. Forms with Signals
  19. HTTP with Signals
  20. Component Communication
  21. State Management
  22. Signals vs RxJS
  23. Performance
  24. Enterprise Folder Structure
  25. Real World Architecture
  26. Flow Diagrams
  27. Complete Learning Roadmap
  28. Interview Questions
  29. Best Practices
  30. Anti-Patterns
  31. Common Mistakes
  32. Cheatsheet
  33. Quiz
  34. Mini Assignments
  35. Summary

Overview

Angular Signals are a reactive primitive introduced to Angular to track state and automatically propagate changes to anything that depends on that state — templates, computed values, and side effects — without relying on Zone.js or manual subscription management.

Before Signals, Angular developers had two broad choices for managing state:

  • Plain class properties, refreshed by Angular's Zone.js-driven change detection.
  • RxJS Observable streams, manually subscribed to and manually unsubscribed from.

Both approaches work, but both come with friction. Zone.js change detection checks the entire component tree on almost every browser event, which is wasteful. RxJS is powerful but has a steep learning curve, and subscription management is a common source of memory leaks for beginners.

Signals solve this by giving Angular fine-grained reactivity: when a signal's value changes, Angular knows exactly which parts of the UI depend on it, and updates only those parts.

Throughout this guide we will build the Employee Management Portal, a realistic internal tool used by the HR department of an IT company named NimbusSoft Technologies, with offices in Pune and Chennai. The application will manage employee records, departments, salary bands, attendance, and search/filter functionality — a domain complex enough to demonstrate every Signals feature in a natural, non-contrived way.

Note If you are coming from React, think of signal() as conceptually similar to useState, computed() as similar to useMemo, and effect() as similar to useEffect — but with automatic dependency tracking. You never declare a dependency array. Angular figures it out by watching which signals you read inside the function.


Why Angular Signals?

Consider a simple requirement in our Employee Management Portal: show the total number of employees in the Pune office, and update it live as employees are added or removed.

Before Signals (plain properties + manual change detection):

export class EmployeeListComponent {
  employees: Employee[] = [];

  get puneEmployeeCount(): number {
    return this.employees.filter(e => e.location === 'Pune').length;
  }
}

This works, but puneEmployeeCount is recalculated on every single change detection cycle, whether employees changed or not. On a large employee list, filtering on every keystroke, every mouse move, and every HTTP response elsewhere in the app is wasteful.

With Signals:

export class EmployeeListComponent {
  employees = signal<Employee[]>([]);

  puneEmployeeCount = computed(() =>
    this.employees().filter(e => e.location === 'Pune').length
  );
}

puneEmployeeCount is now a computed() signal. Angular tracks that it depends on employees. It is recalculated only when employees actually changes, and the recalculated value is cached until the next change. The template binding to puneEmployeeCount() only re-renders when the computed value itself changes.

This is the central promise of Signals: you describe what depends on what, and Angular handles when to recompute and what to re-render.

Key benefits

Benefit Description
Fine-grained reactivity Only the DOM nodes that read a changed signal are updated, not the whole component tree.
No Zone.js required Modern Angular can run zoneless, using Signals as the sole change detection trigger.
Automatic dependency tracking No dependency arrays, no manual subscription/unsubscription.
Synchronous reads signal() values are read synchronously, unlike Observables which require a subscription.
Simpler mental model A signal is just "a box holding a value that notifies readers when it changes."
Works with OnPush by default Signals integrate naturally with ChangeDetectionStrategy.OnPush, which is now the default for new projects.

Problems with Traditional State Management

Let's look at the actual pain points that led Angular to Signals, using situations you will recognize from real projects like our Employee Management Portal.

1. Change detection runs too often

Angular's Zone.js patches asynchronous browser APIs (setTimeout, addEventListener, fetch, Promise, and so on). Every time one of these fires, Zone.js tells Angular "something might have changed," and Angular walks the entire component tree checking bindings for changes. In a large HR portal with dozens of components (employee list, department filter, salary chart, attendance calendar), this becomes expensive.

2. RxJS subscription management is error-prone

export class EmployeeListComponent implements OnInit, OnDestroy {
  private sub!: Subscription;
  employees: Employee[] = [];

  ngOnInit() {
    this.sub = this.employeeService.getEmployees().subscribe(data => {
      this.employees = data;
    });
  }

  ngOnDestroy() {
    this.sub.unsubscribe(); // forget this, and you have a memory leak
  }
}

New Angular developers — including fresher-level engineers joining teams like the one at NimbusSoft — frequently forget ngOnDestroy, causing memory leaks that are hard to detect until the app has been running for a while in production.

3. Derived state is manually recomputed

Without a reactive primitive for derived values, developers often recompute derived data inside ngOnChanges, inside getters (recomputed every change detection cycle), or inside subscription callbacks scattered across the component. This makes it hard to answer a simple question: "where does this value actually come from?"

4. Two different mental models coexist

A codebase mixing plain properties and RxJS Observables forces every developer to remember, for every single field: "is this reactive or not? Do I need | async in the template? Do I need to subscribe?" Signals unify this into one model.

5. Testing reactive logic is harder than it should be

Testing an RxJS-based derived value typically requires fakeAsync, tick(), or TestScheduler. Testing a computed() signal is just calling it like a function and asserting on the result.


Evolution: Zone.js → Change Detection → RxJS → Signals

Understanding why Signals exist requires understanding the road Angular walked to get here.

Step 1: Zone.js and Change Detection

Angular originally shipped with Zone.js, a library that monkey-patches async browser APIs so that Angular can be notified whenever something might affect the UI. When notified, Angular's change detection walks the entire component tree from the root, comparing old and new values for every binding.

This is a push-then-check model: something happens, Angular is pushed a notification, then Angular pulls (checks) every component to see what changed.

flowchart LR
    A[Browser Event: click, timeout, HTTP response] --> B[Zone.js intercepts]
    B --> C[Angular notified: run change detection]
    C --> D[Check entire component tree]
    D --> E[Update DOM where bindings changed]
Loading

Step 2: RxJS enters Angular

To manage asynchronous data — HTTP calls, user input streams, WebSocket messages — Angular adopted RxJS Observables as the standard. RxJS is extremely powerful (operators like debounceTime, switchMap, combineLatest are still unmatched for complex async flows), but it introduced:

  • A learning curve significant enough that many fresher-level developers, such as new joiners at an IT company like NimbusSoft, take months to become comfortable with operators.
  • The requirement to unsubscribe, or use the async pipe, to avoid leaks.
  • A gap between "reactive data" (Observables) and "template-bindable data" (plain properties), bridged awkwardly by the async pipe or manual subscription-to-property assignment.

Step 3: Signals arrive

Signals were designed to give Angular synchronous, fine-grained reactivity without Zone.js and without the ceremony of RxJS, while still allowing RxJS to be used where it genuinely excels (complex async orchestration).

flowchart TB
    subgraph Old Model
    Z[Zone.js] --> CD1[Full Tree Change Detection]
    end
    subgraph New Model
    S[Signal changes] --> CD2[Only dependent nodes re-rendered]
    end
Loading

Comparing the three eras

Era Change Detection Trigger Granularity Async Handling Learning Curve
Zone.js only Any browser event Whole tree Manual / callbacks Low
Zone.js + RxJS Any browser event Whole tree (RxJS just supplies data) Observables, operators High
Signals Signal value changes Fine-grained, per-binding resource(), still can use RxJS Medium

Tip Signals do not replace RxJS. They replace the parts of RxJS that were being used simply to hold and observe a single piece of state (BehaviorSubject used as a store). RxJS remains the right tool for complex async pipelines — merging streams, debouncing search input, retry logic with backoff, and so on.


Learning Roadmap

This is the order in which this README teaches Signals, and the order in which the Employee Management Portal will grow:

  1. Scaffold the Angular project and understand its structure.
  2. Understand why reactivity matters before touching Signal syntax.
  3. Learn signal() — create, read, update, mutate.
  4. Learn computed() — derive values from signals.
  5. Learn effect() — react to signal changes with side effects.
  6. Deep dive into writable signal update patterns.
  7. Learn signal-based input() for parent-to-child communication.
  8. Learn signal-based output() for child-to-parent communication.
  9. Learn linkedSignal() for state that resets/derives conditionally.
  10. Learn resource() for async data fetching tied to signals.
  11. Combine Signals with Reactive Forms.
  12. Fetch employee data over HTTP using signal-friendly patterns.
  13. Study component communication patterns end-to-end.
  14. Build application-wide state using signal-based services.
  15. Compare Signals and RxJS directly, and learn when to use each.
  16. Study performance characteristics and best practices.
  17. Study an enterprise-grade folder structure for a real project.
  18. Study the full application architecture with diagrams.
  19. Review interview questions, best practices, and a cheatsheet.

Project Overview

Application name: Employee Management Portal Company: NimbusSoft Technologies (fictional IT company) Offices: Pune, Chennai Primary users: HR executives and team managers

Core features built across this guide

Feature Signals Concept Demonstrated
List all employees signal(), @for
Filter by department/location computed()
Search by name computed() + effect() for debug logging
Employee detail card input()
Notify parent when employee is edited output()
Salary band that resets when department changes linkedSignal()
Fetch employee list from server resource()
Add/Edit employee form Signals + Reactive Forms
Attendance summary fetched from API resource(), HTTP
Shared employee store across the app Signal-based service

Sample data used throughout this guide

We will consistently reuse this employee dataset in examples:

export interface Employee {
  id: number;
  name: string;
  department: 'Engineering' | 'HR' | 'Finance' | 'Support';
  location: 'Pune' | 'Chennai';
  salary: number;
  email: string;
}

export const SAMPLE_EMPLOYEES: Employee[] = [
  { id: 1, name: 'Suresh Kulkarni', department: 'Engineering', location: 'Pune',    salary: 85000, email: 'suresh.kulkarni@nimbussoft.com' },
  { id: 2, name: 'Ramesh Iyer',     department: 'Finance',     location: 'Chennai', salary: 72000, email: 'ramesh.iyer@nimbussoft.com' },
  { id: 3, name: 'Mahesh Deshpande',department: 'Engineering', location: 'Pune',    salary: 91000, email: 'mahesh.deshpande@nimbussoft.com' },
  { id: 4, name: 'Dinesh Rajan',    department: 'Support',     location: 'Chennai', salary: 55000, email: 'dinesh.rajan@nimbussoft.com' },
  { id: 5, name: 'Kamlesh Verma',   department: 'HR',          location: 'Pune',    salary: 60000, email: 'kamlesh.verma@nimbussoft.com' },
  { id: 6, name: 'Nitesh Sharma',   department: 'Engineering', location: 'Chennai', salary: 88000, email: 'nitesh.sharma@nimbussoft.com' },
];

Folder Structure

Starting structure, right after project generation:

angular-signals-reactive-state/
├── src/
│   ├── app/
│   │   ├── app.component.ts
│   │   ├── app.component.html
│   │   ├── app.config.ts
│   │   └── app.routes.ts
│   ├── index.html
│   ├── main.ts
│   └── styles.css
├── angular.json
├── package.json
└── tsconfig.json

We will grow this into a full feature-based structure in the Enterprise Folder Structure chapter.


Creating the Angular Project

Prerequisites

  • Node.js LTS installed
  • Angular CLI installed globally
npm install -g @angular/cli

Generate the project

ng new angular-signals-reactive-state --standalone --style=css --routing
cd angular-signals-reactive-state

Angular CLI will scaffold a standalone-only application — there is no AppModule. This is intentional for this guide: all components in this project will be standalone, and all state will be Signal-based.

Running the project

ng serve -o

This opens the app at http://localhost:4200.

Generating a component

ng generate component features/employee-list --standalone

Note Since Angular v17+, --standalone is the default when generating components in a standalone project, so you may omit the flag. It is shown here for clarity.


Understanding Reactivity

Before writing a single signal(), you need three mental models solid: reactive programming, push vs pull, and state vs derived state.

Reactive Programming

Reactive programming means describing relationships between values instead of describing steps that update values.

Imperative (non-reactive) style:

let employeeCount = 0;
let pageTitle = '';

function addEmployee() {
  employeeCount++;
  pageTitle = `Employees (${employeeCount})`; // must remember to update this manually
}

Reactive style:

const employeeCount = signal(0);
const pageTitle = computed(() => `Employees (${employeeCount()})`);

In the reactive version, pageTitle is declared as depending on employeeCount. You never have to remember to update it — it updates itself.

Push vs Pull

  • Pull-based systems (plain getters, Zone.js change detection) ask "has anything changed?" repeatedly, whether or not anything actually changed.
  • Push-based systems (Signals, RxJS) are told "this changed" exactly when it happens, and only the interested parties react.

Signals are push-based at the notification level, but pull-based (lazy) at the computation level: a computed() signal doesn't recalculate the instant its dependency changes — it recalculates the next time someone reads it. This hybrid model is what makes Signals both efficient and simple to reason about.

State vs Derived State

  • State is a signal you write to directly: employees, searchTerm, selectedDepartment.
  • Derived state is a signal computed from other signals: filteredEmployees, puneEmployeeCount, averageSalary.

A common beginner mistake — one you will see junior developers make in almost every Angular team, including at NimbusSoft — is storing derived state as its own signal() and manually keeping it in sync:

// Avoid this
employees = signal<Employee[]>([]);
puneCount = signal(0);

addEmployee(e: Employee) {
  this.employees.update(list => [...list, e]);
  this.puneCount.set(this.employees().filter(x => x.location === 'Pune').length); // easy to forget
}

Instead, derive it:

employees = signal<Employee[]>([]);
puneCount = computed(() => this.employees().filter(x => x.location === 'Pune').length);

Mutable vs Immutable Updates

Signals detect changes by reference comparison by default (Object.is). This means:

employees = signal<Employee[]>([]);

// WRONG — mutating the existing array reference; Signals will NOT notify consumers
this.employees().push(newEmployee);

// RIGHT — create a new array reference
this.employees.update(list => [...list, newEmployee]);

Warning Mutating an object or array returned by a signal and expecting the UI to update is one of the most common Signals bugs. Always produce a new reference through .set() or .update().


Signals Fundamentals

What is a Signal?

A signal is a wrapper around a value that:

  1. Lets you read the current value by calling it as a function: mySignal().
  2. Lets you write a new value: mySignal.set(newValue) or mySignal.update(fn).
  3. Notifies any computed(), effect(), or template binding that read it, whenever the value changes.

Why Signal?

Because it turns "a value that changes" into "a value whose changes are trackable," without you writing any explicit subscription code.

How Signal Works (Conceptually)

Internally, a signal keeps:

  • The current value.
  • A version counter, bumped on every .set()/.update() call that produces a different value.
  • A list of "consumers" (computed signals, effects, template bindings) that read it.

When you call mySignal() inside a reactive context (a computed(), an effect(), or a component template), Angular records "this consumer depends on this signal." The next time the signal's version changes, all recorded consumers are marked dirty and eventually re-evaluated.

Signal Lifecycle

sequenceDiagram
    participant C as Component Class
    participant S as Signal
    participant T as Template

    C->>S: signal(initialValue)
    T->>S: read via mySignal()
    S-->>T: current value, dependency recorded
    C->>S: mySignal.set(newValue)
    S->>T: notify — mark dirty
    T->>S: read again on next render
    S-->>T: new value
Loading

Creating a Signal

import { signal } from '@angular/core';

export class EmployeeListComponent {
  employees = signal<Employee[]>(SAMPLE_EMPLOYEES);
  searchTerm = signal<string>('');
  selectedLocation = signal<'Pune' | 'Chennai' | 'All'>('All');
}

Reading a Signal

Signals are read by calling them as functions:

console.log(this.employees().length); // 6

In templates:

<p>Total employees: {{ employees().length }}</p>

Updating a Signal

Two methods exist for writing:

// .set() replaces the value entirely
this.searchTerm.set('Suresh');

// .update() derives the new value from the old one
this.employees.update(list => [...list, newEmployee]);

"Mutating" a Signal (and why you generally should not)

There is no .mutate() method on signals for objects/arrays in modern Angular (it was removed from the public writable-signal API in favor of .update() with immutable patterns). Always use .update() with a spread/copy:

// Adding an employee
this.employees.update(list => [...list, newEmployee]);

// Removing an employee
this.employees.update(list => list.filter(e => e.id !== employeeId));

// Updating one employee's salary
this.employees.update(list =>
  list.map(e => e.id === employeeId ? { ...e, salary: e.salary + 5000 } : e)
);

Nested Objects

For nested state, spread at every level you are changing:

interface EmployeeProfile {
  employee: Employee;
  address: { city: string; pincode: string };
}

profile = signal<EmployeeProfile>({
  employee: SAMPLE_EMPLOYEES[0],
  address: { city: 'Pune', pincode: '411001' }
});

updateCity(newCity: string) {
  this.profile.update(p => ({
    ...p,
    address: { ...p.address, city: newCity }
  }));
}

Arrays

Common array operations, all producing new references:

// Add
employees.update(list => [...list, newEmployee]);

// Remove by id
employees.update(list => list.filter(e => e.id !== id));

// Update one item
employees.update(list => list.map(e => e.id === id ? { ...e, ...changes } : e));

// Sort (creates a new array first!)
employees.update(list => [...list].sort((a, b) => a.salary - b.salary));

// Clear
employees.set([]);

Best Practices

  • Treat every signal's value as immutable — never mutate in place.
  • Keep signals narrow and specific — prefer several small signals over one giant "app state" signal, unless you are deliberately building a store.
  • Name signals after what they hold, not how they are used: employees, not employeeData1.
  • Prefer computed() over storing and manually syncing derived values.

Common Mistakes

Mistake Why It's Wrong Fix
this.employees().push(x) Mutates in place, no new reference, no update notification this.employees.update(list => [...list, x])
Calling a signal without () in a template Template shows [object Object] or a function reference Always call it: employees()
Creating signals inside methods, not as class fields Signal identity resets every call, breaking reactivity Declare signals as class fields
Reading a signal outside of an injection context in a constructor-dependent API like effect() without proper context Runtime error about missing injection context Call effect() inside constructor or field initializer, or pass { injector }

Performance Notes

  • Reading a signal is an O(1) operation — it is not expensive to call employees() many times.
  • Angular deduplicates multiple reads of the same signal within one change detection pass.
  • Signals pair naturally with ChangeDetectionStrategy.OnPush, since Angular can skip checking a component entirely if none of the signals it reads have changed.

Computed Signals

computed()

A computed() signal derives its value from other signals. It is read-only — you cannot .set() a computed signal.

import { signal, computed } from '@angular/core';

employees = signal<Employee[]>(SAMPLE_EMPLOYEES);
selectedLocation = signal<'Pune' | 'Chennai' | 'All'>('All');

filteredEmployees = computed(() => {
  const location = this.selectedLocation();
  const list = this.employees();
  return location === 'All' ? list : list.filter(e => e.location === location);
});

Dependencies Are Tracked Automatically

Angular does not need you to declare [employees, selectedLocation] as dependencies. It simply notices, while executing the computed() function, that you called this.employees() and this.selectedLocation(), and records both as dependencies.

Warning Dependency tracking only works for signals read synchronously during the computed function's execution. A signal read inside a setTimeout or after an await inside a computed function will not be tracked.

Lazy Evaluation

A computed() signal does not run its derivation function until it is read for the first time. If filteredEmployees is never read by any template or effect, its function body never executes, no matter how many times employees changes.

Caching

Once computed, the result is cached. If you read filteredEmployees() five times in a row without any dependency changing, the derivation function runs once, and the cached value is returned four more times.

console.log(this.filteredEmployees()); // runs the function
console.log(this.filteredEmployees()); // returns cached result, function does NOT run again

Nested Computed Signals

Computed signals can depend on other computed signals, forming a dependency graph:

employees = signal<Employee[]>(SAMPLE_EMPLOYEES);
selectedLocation = signal<'Pune' | 'Chennai' | 'All'>('All');

filteredEmployees = computed(() =>
  this.selectedLocation() === 'All'
    ? this.employees()
    : this.employees().filter(e => e.location === this.selectedLocation())
);

averageSalary = computed(() => {
  const list = this.filteredEmployees();
  if (list.length === 0) return 0;
  const total = list.reduce((sum, e) => sum + e.salary, 0);
  return Math.round(total / list.length);
});

averageSalaryFormatted = computed(() =>
  `₹${this.averageSalary().toLocaleString('en-IN')}`
);

Here, averageSalaryFormatted depends on averageSalary, which depends on filteredEmployees, which depends on employees and selectedLocation. Changing selectedLocation ripples through the whole chain automatically.

flowchart LR
    E[employees signal] --> F[filteredEmployees computed]
    L[selectedLocation signal] --> F
    F --> A[averageSalary computed]
    A --> AF[averageSalaryFormatted computed]
Loading

Examples in Our Portal

searchTerm = signal('');

searchedEmployees = computed(() => {
  const term = this.searchTerm().trim().toLowerCase();
  const list = this.filteredEmployees();
  if (!term) return list;
  return list.filter(e => e.name.toLowerCase().includes(term));
});

Template:

<input [ngModel]="searchTerm()" (ngModelChange)="searchTerm.set($event)" placeholder="Search employee, e.g. Ramesh" />

@for (emp of searchedEmployees(); track emp.id) {
  <div class="employee-card">{{ emp.name }} — {{ emp.department }}, {{ emp.location }}</div>
} @empty {
  <p>No employees found.</p>
}

Effects

effect()

An effect runs a side effect whenever any signal it reads changes. Unlike computed(), effects do not produce a value — they exist purely for side effects: logging, saving to localStorage, syncing with a non-Signal API, and similar tasks.

import { effect } from '@angular/core';

export class EmployeeListComponent {
  selectedLocation = signal<'Pune' | 'Chennai' | 'All'>('All');

  constructor() {
    effect(() => {
      console.log(`Location filter changed to: ${this.selectedLocation()}`);
    });
  }
}

Side Effects

Typical use cases in our Employee Management Portal:

constructor() {
  // Persist the last used filter to localStorage
  effect(() => {
    localStorage.setItem('lastLocationFilter', this.selectedLocation());
  });

  // Sync page title with employee count
  effect(() => {
    document.title = `Employee Portal (${this.employees().length})`;
  });
}

Cleanup

Effects can register a cleanup function, run before the next execution and on destruction:

constructor() {
  effect((onCleanup) => {
    const location = this.selectedLocation();
    const timer = setTimeout(() => {
      console.log(`Still viewing ${location} after 5 seconds`);
    }, 5000);

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

DestroyRef

Effects created in a constructor or field initializer are automatically tied to the component/service's lifecycle and cleaned up on destroy. If you must create an effect outside that context (for example, inside a method), you must provide an injector explicitly:

import { DestroyRef, Injector, inject, effect } from '@angular/core';

export class EmployeeListComponent {
  private injector = inject(Injector);

  startWatching() {
    effect(() => {
      console.log(this.employees().length);
    }, { injector: this.injector });
  }
}

Common Mistakes

Mistake Problem Fix
Using effect() to derive a value Effects don't return values meant for templates Use computed() instead
Writing to a signal inside an effect() that also reads that same signal Can cause infinite loops Avoid, or use allowSignalWrites deliberately and carefully, restructuring logic where possible
Creating effect() outside constructor without an injector Runtime "injection context" error Pass { injector } explicitly
Using effect() for HTTP calls tied to a signal Manual subscription management creeps back in Prefer resource() (see later chapter)

Examples

// Logging every employee count change for a Chennai team lead's dashboard
constructor() {
  effect(() => {
    const chennaiCount = this.employees().filter(e => e.location === 'Chennai').length;
    console.log(`Chennai office headcount: ${chennaiCount}`);
  });
}

Writable Signals

A writable signal is what signal() creates — as opposed to computed(), which produces a read-only signal.

Update

.update() receives the current value and returns the new value:

salaryHike = signal(0);
salaryHike.update(current => current + 5000);

Set

.set() replaces the value directly:

selectedLocation.set('Chennai');

"Mutate" (Immutable Patterns Instead)

As covered earlier, there is no in-place mutate API for objects/arrays on the public writable signal in current Angular — always express changes immutably:

// Give every Engineering employee in Pune a 10% raise
employees.update(list =>
  list.map(e =>
    e.department === 'Engineering' && e.location === 'Pune'
      ? { ...e, salary: Math.round(e.salary * 1.10) }
      : e
  )
);

Read-only Views

You can expose a read-only view of a writable signal using .asReadonly(), a common pattern for signal-based services (see State Management):

private _employees = signal<Employee[]>(SAMPLE_EMPLOYEES);
readonly employees = this._employees.asReadonly();

External consumers can read employees() but cannot call .set() on it — only the service itself can mutate _employees.


Input Signals

Signal Inputs

Modern Angular replaces the @Input() decorator with the input() function, giving you a signal instead of a plain property.

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

@Component({
  selector: 'app-employee-card',
  standalone: true,
  template: `
    <div class="card">
      <h3>{{ employee().name }}</h3>
      <p>{{ employee().department }} — {{ employee().location }}</p>
      <p>₹{{ employee().salary.toLocaleString('en-IN') }}</p>
    </div>
  `
})
export class EmployeeCardComponent {
  employee = input.required<Employee>();
  highlight = input<boolean>(false); // optional, with default value
}

Usage from a parent:

<app-employee-card
  *ngFor="let emp of employees()"
  [employee]="emp"
  [highlight]="emp.location === 'Pune'"
/>

Or with modern control flow:

@for (emp of employees(); track emp.id) {
  <app-employee-card [employee]="emp" [highlight]="emp.location === 'Pune'" />
}

Required Inputs

input.required<T>() guarantees, at compile time, that the parent must bind this input. Omitting it is a template type-checking error, not just a runtime warning — a major improvement for a team of freshers who might otherwise forget to pass required data.

Transform and Alias

name = input('', { alias: 'employeeName' });
isVip = input(false, { transform: (v: string | boolean) => !!v });

Component Communication via Input Signals

Because employee is a signal, you can build computed() values directly from an input inside the child:

export class EmployeeCardComponent {
  employee = input.required<Employee>();

  salaryBand = computed(() => {
    const salary = this.employee().salary;
    if (salary >= 90000) return 'Senior';
    if (salary >= 70000) return 'Mid';
    return 'Junior';
  });
}

This was awkward before signal inputs — you needed ngOnChanges to react to input changes. Now, computed() reacts automatically because employee is a signal.


Output Signals

The output() Function

Modern Angular replaces @Output() + EventEmitter with the output() function.

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

@Component({
  selector: 'app-employee-card',
  standalone: true,
  template: `
    <div class="card">
      <h3>{{ employee().name }}</h3>
      <button (click)="onEdit()">Edit</button>
      <button (click)="onDelete()">Delete</button>
    </div>
  `
})
export class EmployeeCardComponent {
  employee = input.required<Employee>();

  edit = output<Employee>();
  delete = output<number>(); // emits employee id

  onEdit() {
    this.edit.emit(this.employee());
  }

  onDelete() {
    this.delete.emit(this.employee().id);
  }
}

Parent usage:

@for (emp of employees(); track emp.id) {
  <app-employee-card
    [employee]="emp"
    (edit)="openEditForm($event)"
    (delete)="removeEmployee($event)"
  />
}
export class EmployeeListComponent {
  employees = signal<Employee[]>(SAMPLE_EMPLOYEES);

  removeEmployee(id: number) {
    this.employees.update(list => list.filter(e => e.id !== id));
  }

  openEditForm(employee: Employee) {
    this.employeeBeingEdited.set(employee);
  }
}

Communication Patterns Summary

Direction Mechanism Angular API
Parent → Child Pass data down input() / input.required()
Child → Parent Emit events up output()
Sibling → Sibling Share a common ancestor's state, or a shared service Signal-based service
Deep hierarchy Shared, injectable, signal-based state Signal-based service (see State Management)

Linked Signals

When to Use linkedSignal()

linkedSignal() creates a writable signal whose default value is derived from another signal, but which the user can subsequently override — and which automatically resets when its source signal changes.

This is exactly the situation we hit in our portal's Add/Edit Employee form: the salary band dropdown default should follow the selected department, but the HR executive should still be able to manually override it — until they change the department again, at which point it should reset to that department's default band.

import { signal, linkedSignal } from '@angular/core';

department = signal<'Engineering' | 'HR' | 'Finance' | 'Support'>('Engineering');

defaultBandFor = (dept: string) => {
  switch (dept) {
    case 'Engineering': return 'Band 3';
    case 'Finance': return 'Band 2';
    default: return 'Band 1';
  }
};

selectedBand = linkedSignal(() => this.defaultBandFor(this.department()));

Behavior:

  • Initially, selectedBand() is 'Band 3' (Engineering's default).
  • The HR executive can call selectedBand.set('Band 4') manually — it behaves like a normal writable signal.
  • If department changes to 'Finance', selectedBand automatically resets to 'Band 2', discarding the manual override, because its source changed.

Real Example: Add Employee Form

export class AddEmployeeFormComponent {
  department = signal<Employee['department']>('Engineering');

  // Reset location suggestion whenever department changes,
  // but let the HR user override it if needed
  suggestedLocation = linkedSignal(() =>
    this.department() === 'Support' ? 'Chennai' : 'Pune'
  );
}

linkedSignal() with Previous Value Access

A more advanced form lets you inspect the previous source and previous value, useful for "keep the selection if it's still valid" logic:

selectedEmployeeId = linkedSignal<number[], number | null>({
  source: () => this.filteredEmployeeIds(),
  computation: (newIds, previous) => {
    // Keep previous selection if it still exists in the new filtered list
    if (previous && newIds.includes(previous.value)) {
      return previous.value;
    }
    return newIds[0] ?? null;
  }
});

This solves a real bug class: an HR executive has an employee selected, changes the department filter, and the previously selected employee happens to still be in the new filtered list — so the selection should be preserved instead of being reset to null.


Resource API

Async State with resource()

resource() ties an asynchronous operation (typically an HTTP call) to a signal, so that whenever the signal changes, the async operation re-runs automatically, and Angular exposes value, status, error, and isLoading as signals for you.

import { resource } from '@angular/core';

export class EmployeeListComponent {
  selectedLocation = signal<'Pune' | 'Chennai'>('Pune');

  employeesResource = resource({
    request: () => ({ location: this.selectedLocation() }),
    loader: async ({ request }) => {
      const response = await fetch(`/api/employees?location=${request.location}`);
      if (!response.ok) throw new Error('Failed to load employees');
      return response.json() as Promise<Employee[]>;
    }
  });
}

Template:

@if (employeesResource.isLoading()) {
  <p>Loading employees for {{ selectedLocation() }}...</p>
} @else if (employeesResource.error()) {
  <p class="error">Could not load employees. Please try again.</p>
} @else {
  @for (emp of employeesResource.value() ?? []; track emp.id) {
    <app-employee-card [employee]="emp" />
  }
}

Loading / Success / Error / Cancellation

resource() exposes:

Signal Meaning
.value() The last successfully loaded data, or undefined
.status() 'idle' | 'loading' | 'reloading' | 'resolved' | 'error'
.error() The error object, if the loader threw
.isLoading() true while a request is in flight

Cancellation: if selectedLocation changes while a previous request is still in flight, resource() automatically discards the stale response when it eventually arrives, so the UI never shows Chennai data while selectedLocation reads 'Pune'.

Manually Reloading

<button (click)="employeesResource.reload()">Refresh</button>

Real Scenario: Attendance Summary for a Selected Employee

selectedEmployeeId = signal<number | null>(null);

attendanceResource = resource({
  request: () => this.selectedEmployeeId(),
  loader: async ({ request: employeeId }) => {
    if (employeeId === null) return null;
    const res = await fetch(`/api/attendance/${employeeId}`);
    if (!res.ok) throw new Error('Attendance fetch failed');
    return res.json() as Promise<AttendanceSummary>;
  }
});

Selecting Dinesh Rajan from the employee list sets selectedEmployeeId.set(4), which automatically triggers attendanceResource's loader with employeeId = 4 — no manual subscription, no manual cancellation logic.


Forms with Signals

Reactive Forms + Signals

Angular's FormControl/FormGroup are not themselves Signals, but they expose valueChanges as an Observable, which you can bridge into a signal using toSignal(), and signals can drive form defaults.

import { FormBuilder, Validators } from '@angular/forms';
import { toSignal } from '@angular/core/rxjs-interop';

export class AddEmployeeFormComponent {
  private fb = inject(FormBuilder);

  form = this.fb.group({
    name: ['', Validators.required],
    department: ['Engineering', Validators.required],
    location: ['Pune', Validators.required],
    salary: [50000, [Validators.required, Validators.min(10000)]],
  });

  formValue = toSignal(this.form.valueChanges, { initialValue: this.form.value });

  isValid = computed(() => this.form.valid); // form.valid isn't itself a signal, read carefully — see note below
}

Note form.valid is a plain getter, not a signal, so wrapping it in computed() alone will not make it reactive. Prefer deriving validity from toSignal(this.form.statusChanges, ...) if you need it as a genuinely reactive signal:

formStatus = toSignal(this.form.statusChanges, { initialValue: this.form.status });
isValid = computed(() => this.formStatus() === 'VALID');

Template-Driven Forms with Signals

<input
  name="employeeName"
  [ngModel]="name()"
  (ngModelChange)="name.set($event)"
  required
/>
name = signal('');

A Complete Add Employee Form Pattern

export class AddEmployeeFormComponent {
  name = signal('');
  department = signal<Employee['department']>('Engineering');
  location = linkedSignal(() => this.department() === 'Support' ? 'Chennai' : 'Pune');
  salary = signal(50000);

  isFormValid = computed(() =>
    this.name().trim().length > 2 && this.salary() >= 10000
  );

  submit = output<Employee>();

  onSubmit() {
    if (!this.isFormValid()) return;
    this.submit.emit({
      id: Date.now(),
      name: this.name(),
      department: this.department(),
      location: this.location(),
      salary: this.salary(),
      email: `${this.name().toLowerCase().replace(/\s+/g, '.')}@nimbussoft.com`
    });
  }
}
<form (ngSubmit)="onSubmit()">
  <input [ngModel]="name()" (ngModelChange)="name.set($event)" name="name" placeholder="e.g. Hitesh Menon" />

  <select [ngModel]="department()" (ngModelChange)="department.set($event)" name="department">
    <option value="Engineering">Engineering</option>
    <option value="HR">HR</option>
    <option value="Finance">Finance</option>
    <option value="Support">Support</option>
  </select>

  <p>Suggested location: {{ location() }}</p>

  <input type="number" [ngModel]="salary()" (ngModelChange)="salary.set($event)" name="salary" />

  <button type="submit" [disabled]="!isFormValid()">Add Employee</button>
</form>

HTTP with Signals

Fetching Data

The recommended pattern is resource() for anything driven by another signal (see Resource API). For a single, one-time fetch not tied to changing parameters, toSignal() bridging an HttpClient Observable also works:

import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';

export class EmployeeListComponent {
  private http = inject(HttpClient);

  employees = toSignal(
    this.http.get<Employee[]>('/api/employees'),
    { initialValue: [] as Employee[] }
  );
}

Loading and Error Handling with resource()

department = signal<Employee['department']>('Engineering');

employeesByDept = resource({
  request: () => this.department(),
  loader: async ({ request: dept }) => {
    const res = await fetch(`/api/employees?department=${dept}`);
    if (!res.ok) throw new Error(`Server returned ${res.status}`);
    return res.json() as Promise<Employee[]>;
  }
});

Retry Pattern

resource()'s loader is a plain async function, so retry logic is just normal async/await code:

loader: async ({ request: dept }) => {
  const maxAttempts = 3;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      const res = await fetch(`/api/employees?department=${dept}`);
      if (!res.ok) throw new Error(`Attempt ${attempt} failed`);
      return res.json() as Promise<Employee[]>;
    } catch (err) {
      if (attempt === maxAttempts) throw err;
      await new Promise(r => setTimeout(r, attempt * 500)); // simple backoff
    }
  }
  throw new Error('Unreachable');
}

Caching Pattern

For simple caching keyed by request parameters, a signal-based map works well alongside resource():

private cache = new Map<string, Employee[]>();

employeesByDept = resource({
  request: () => this.department(),
  loader: async ({ request: dept }) => {
    if (this.cache.has(dept)) return this.cache.get(dept)!;
    const res = await fetch(`/api/employees?department=${dept}`);
    const data = await res.json() as Employee[];
    this.cache.set(dept, data);
    return data;
  }
});

Component Communication

This chapter ties input(), output(), and shared services together in one worked example: the Employee List → Employee Card → Edit Form flow.

Parent → Child (Input Signals)

<!-- employee-list.component.html -->
@for (emp of searchedEmployees(); track emp.id) {
  <app-employee-card [employee]="emp" (edit)="startEdit($event)" (delete)="remove($event)" />
}

Child → Parent (Output Signals)

// employee-card.component.ts
edit = output<Employee>();
delete = output<number>();

Sibling Communication (Through a Shared Service)

The Employee List and the Attendance Widget are siblings under app.component.ts. Neither is the other's parent or child, so they communicate through a shared, injectable, signal-based service:

@Injectable({ providedIn: 'root' })
export class EmployeeSelectionService {
  private _selectedEmployeeId = signal<number | null>(null);
  readonly selectedEmployeeId = this._selectedEmployeeId.asReadonly();

  select(id: number) {
    this._selectedEmployeeId.set(id);
  }
}
// employee-list.component.ts
export class EmployeeListComponent {
  private selection = inject(EmployeeSelectionService);

  onCardClick(emp: Employee) {
    this.selection.select(emp.id);
  }
}
// attendance-widget.component.ts
export class AttendanceWidgetComponent {
  private selection = inject(EmployeeSelectionService);

  attendanceResource = resource({
    request: () => this.selection.selectedEmployeeId(),
    loader: async ({ request: id }) => id === null ? null : fetchAttendance(id)
  });
}

Clicking an employee in the list updates selectedEmployeeId in the shared service; the sibling attendance widget's resource() reacts automatically — no @Output chains, no event bus, no Subject.

Shared State Diagram

flowchart TB
    subgraph AppComponent
      EL[EmployeeListComponent]
      AW[AttendanceWidgetComponent]
    end
    SVC[(EmployeeSelectionService signal)]
    EL -- selects employee --> SVC
    SVC -- selectedEmployeeId signal --> AW
Loading

State Management

Without RxJS: Pure Signal-Based Store

@Injectable({ providedIn: 'root' })
export class EmployeeStore {
  private _employees = signal<Employee[]>(SAMPLE_EMPLOYEES);
  readonly employees = this._employees.asReadonly();

  private _selectedLocation = signal<'All' | 'Pune' | 'Chennai'>('All');
  readonly selectedLocation = this._selectedLocation.asReadonly();

  readonly filteredEmployees = computed(() => {
    const loc = this._selectedLocation();
    const list = this._employees();
    return loc === 'All' ? list : list.filter(e => e.location === loc);
  });

  readonly totalPayroll = computed(() =>
    this._employees().reduce((sum, e) => sum + e.salary, 0)
  );

  setLocationFilter(loc: 'All' | 'Pune' | 'Chennai') {
    this._selectedLocation.set(loc);
  }

  addEmployee(e: Employee) {
    this._employees.update(list => [...list, e]);
  }

  removeEmployee(id: number) {
    this._employees.update(list => list.filter(e => e.id !== id));
  }

  giveRaise(id: number, amount: number) {
    this._employees.update(list =>
      list.map(e => e.id === id ? { ...e, salary: e.salary + amount } : e)
    );
  }
}

Every feature component now injects EmployeeStore instead of holding its own copy of employee data — a single source of truth, with private write access (_employees) and public read-only access (employees).

Hybrid: Signals + RxJS

For genuinely async, multi-step flows (say, a live WebSocket feed of attendance check-ins from Pune and Chennai offices), RxJS remains the right tool, bridged into signals at the boundary:

@Injectable({ providedIn: 'root' })
export class AttendanceFeedService {
  private socket$ = webSocket<AttendanceEvent>('wss://api.nimbussoft.com/attendance-feed');

  readonly latestEvent = toSignal(this.socket$, { initialValue: null });
}

Global State vs Feature State

Type Example in this Project Where it Lives
Global state Logged-in HR user, theme, EmployeeStore providedIn: 'root' services
Feature state Search term on the Employee List page, form draft values Component-level signals, not shared

Tip Not everything needs to be global. searchTerm in the Employee List component is a perfect example of state that should stay local to that component — promoting it to a global store adds unnecessary coupling.


Signals vs RxJS

Comparison Table

Aspect Signals RxJS
Reading value Synchronous: mySignal() Requires subscription or async pipe
Multiple values over time Not designed for streams of discrete events Core strength — Subject, fromEvent, etc.
Derived values computed() — simple, cached combineLatest + map — more ceremony
Cancellation Built into resource() Requires operators like switchMap
Learning curve Low to medium Medium to high
Debounce/throttle/complex async orchestration Not built-in Rich operator library
Unsubscription risk None — no explicit subscriptions Real risk if not using async pipe / takeUntilDestroyed
Best for Component/UI state, derived UI values Complex async streams, multi-source coordination

When to Use Signals

  • Holding component or store state (employees, searchTerm, selectedLocation).
  • Deriving UI values (filteredEmployees, averageSalary).
  • Reacting to state with side effects (effect() for logging, persistence).
  • Fetching data whose parameters are also signals (resource()).

When to Use RxJS

  • Debouncing a search input against a live server search endpoint.
  • Merging multiple async sources (e.g., attendance events from Pune and Chennai offices arriving on separate WebSocket channels).
  • Complex retry/backoff/race conditions across multiple requests.
  • Anything requiring operators like switchMap, debounceTime, distinctUntilChanged, merge, combineLatest in combination.

When to Combine Both

A very common real pattern: debounce user input with RxJS, then expose the debounced value as a signal for the rest of the component to consume.

private searchInput$ = new Subject<string>();

readonly debouncedSearchTerm = toSignal(
  this.searchInput$.pipe(debounceTime(300), distinctUntilChanged()),
  { initialValue: '' }
);

onSearchInput(value: string) {
  this.searchInput$.next(value);
}

searchedEmployees = computed(() => {
  const term = this.debouncedSearchTerm().toLowerCase();
  return this.employees().filter(e => e.name.toLowerCase().includes(term));
});

Performance

Rendering

Because computed() and template bindings only re-evaluate when their tracked signals change, the Employee Management Portal's employee list only re-renders the specific <app-employee-card> whose bound employee input actually changed reference — not the entire list — as long as each card uses OnPush (which is the default for signal-based components generated by modern Angular CLI).

Change Detection

With Signals, Angular can increasingly operate zoneless (no Zone.js at all), since signal writes themselves can schedule the necessary change detection, rather than relying on patched async APIs to guess that "something might have changed."

Memory

  • effect()s created in a constructor are automatically cleaned up when the component/service is destroyed — no manual ngOnDestroy bookkeeping.
  • Signals themselves are lightweight; holding hundreds of signals (e.g., one per employee row for an editable grid) is generally fine, but prefer one signal holding an array over hundreds of individual signals unless you specifically need per-row granularity.

Best Practices for Performance

  • Keep computed() functions pure and cheap — avoid expensive operations like deep cloning inside a computed that runs frequently.
  • Avoid creating new object/array literals inside a template expression directly (e.g., [data]="{...}" inline) — prefer a computed() so the reference is stable between renders when nothing changed.
  • Prefer many small, targeted signals over one large "everything" signal object, so that unrelated changes don't force wide recomputation.
  • Use track emp.id (not track $index) in @for loops so Angular can correctly reuse DOM nodes when the underlying array changes.

Enterprise Folder Structure

By the end of this guide, the Employee Management Portal grows into this feature-based structure:

angular-signals-reactive-state/
├── src/
│   ├── app/
│   │   ├── app.component.ts
│   │   ├── app.config.ts
│   │   ├── app.routes.ts
│   │   │
│   │   ├── core/
│   │   │   ├── services/
│   │   │   │   ├── employee-store.service.ts
│   │   │   │   ├── employee-selection.service.ts
│   │   │   │   └── attendance-feed.service.ts
│   │   │   └── models/
│   │   │       ├── employee.model.ts
│   │   │       └── attendance.model.ts
│   │   │
│   │   ├── features/
│   │   │   ├── employee-list/
│   │   │   │   ├── employee-list.component.ts
│   │   │   │   ├── employee-list.component.html
│   │   │   │   └── employee-card/
│   │   │   │       ├── employee-card.component.ts
│   │   │   │       └── employee-card.component.html
│   │   │   │
│   │   │   ├── employee-form/
│   │   │   │   ├── add-employee-form.component.ts
│   │   │   │   └── add-employee-form.component.html
│   │   │   │
│   │   │   └── attendance/
│   │   │       ├── attendance-widget.component.ts
│   │   │       └── attendance-widget.component.html
│   │   │
│   │   └── shared/
│   │       ├── pipes/
│   │       └── directives/
│   │
│   ├── index.html
│   ├── main.ts
│   └── styles.css
├── angular.json
├── package.json
└── tsconfig.json

Tip core/ holds application-wide singletons (providedIn: 'root' services and shared models). features/ holds self-contained feature folders, each owning its own components. shared/ holds truly generic, reusable pieces (pipes, directives) with no feature-specific knowledge.


Real World Architecture

flowchart TB
    subgraph Presentation Layer
      ELC[EmployeeListComponent]
      ECC[EmployeeCardComponent]
      AEF[AddEmployeeFormComponent]
      AWC[AttendanceWidgetComponent]
    end

    subgraph State Layer
      ES[EmployeeStore signal service]
      SS[EmployeeSelectionService]
    end

    subgraph Data Layer
      API[(NimbusSoft Employee API)]
    end

    ELC -- reads employees, filteredEmployees --> ES
    ELC --> ECC
    ECC -- edit / delete outputs --> ELC
    AEF -- addEmployee --> ES
    ELC -- select --> SS
    AWC -- reads selectedEmployeeId --> SS
    AWC -- resource fetch --> API
    ES -- resource fetch --> API
Loading

Layered Responsibilities

Layer Responsibility Signals Used
Presentation Render UI, handle user interaction input(), output(), template signal reads
State Hold and derive application data signal(), computed(), linkedSignal()
Data Talk to the backend API resource(), HttpClient + toSignal()

Flow Diagrams

Signal Update Flow

sequenceDiagram
    participant User
    participant Component
    participant Signal
    participant Template

    User->>Component: Clicks "Give Raise" on Suresh's card
    Component->>Signal: employees.update(list => ...)
    Signal->>Signal: Compare reference, detect change
    Signal->>Template: Notify dependent bindings
    Template->>Template: Re-render only Suresh's salary cell
Loading

Computed Flow

sequenceDiagram
    participant Signal as employees signal
    participant Computed as averageSalary computed
    participant Template

    Signal->>Computed: Marked dirty on change
    Template->>Computed: Reads averageSalary()
    Computed->>Computed: Recomputes (was dirty)
    Computed-->>Template: Returns new cached value
Loading

Effect Flow

sequenceDiagram
    participant Signal as selectedLocation signal
    participant Effect

    Signal->>Effect: Value changed to 'Chennai'
    Effect->>Effect: Scheduled to run after current change detection
    Effect->>Effect: Executes side effect (e.g., localStorage.setItem)
Loading

Component Communication Flow

flowchart LR
    Parent[EmployeeListComponent] -- input: employee --> Child[EmployeeCardComponent]
    Child -- output: edit, delete --> Parent
Loading

HTTP Flow (via resource())

sequenceDiagram
    participant Signal as department signal
    participant Resource as employeesByDept resource
    participant API

    Signal->>Resource: department changed to 'Finance'
    Resource->>API: fetch('/api/employees?department=Finance')
    API-->>Resource: JSON response
    Resource->>Resource: value() updated, isLoading() false
Loading

State Flow (Store Pattern)

flowchart TB
    Form[AddEmployeeFormComponent] -- addEmployee(e) --> Store[EmployeeStore]
    List[EmployeeListComponent] -- reads employees, filteredEmployees --> Store
    Card[EmployeeCardComponent] -- giveRaise(id, amount) --> Store
Loading

Complete Learning Roadmap

A consolidated checklist you can use to track your own progress:

  • Understand push vs pull and state vs derived state
  • Create, read, update signals
  • Understand immutability requirements for objects/arrays in signals
  • Build computed() chains
  • Understand lazy evaluation and caching of computed()
  • Use effect() for side effects, with cleanup
  • Understand injection context requirements for effect()
  • Use input() and input.required() for parent-to-child data
  • Use output() for child-to-parent events
  • Use linkedSignal() for resettable derived defaults
  • Use resource() for signal-driven async data fetching
  • Combine Reactive Forms with signals via toSignal()
  • Build a signal-based store service with private write / public read-only signals
  • Understand when RxJS is still the better tool
  • Apply OnPush and immutable patterns for performance
  • Structure a real project using feature-based folders

Interview Questions

Beginner

  1. What is a Signal in Angular, and how do you read its value?
  2. What is the difference between signal() and computed()?
  3. Why do you call a signal as a function, e.g. employees(), instead of accessing it as a property?
  4. What happens if you mutate an array returned by a signal directly instead of using .update()?
  5. What is the difference between .set() and .update()?

Intermediate

  1. Explain how Angular tracks dependencies for a computed() signal without an explicit dependency array.
  2. Why is a computed() signal described as "lazily evaluated and cached"? Give an example where this matters.
  3. What is the purpose of the cleanup function inside effect(), and when would you use it?
  4. Why might you get an "injection context" error when calling effect(), and how do you fix it?
  5. How does input.required<T>() improve on the traditional @Input() decorator?

Advanced

  1. Compare Signals and RxJS in terms of how they handle multiple values over time versus a single current value.
  2. Explain how resource() handles request cancellation when its source signal changes rapidly, using the Employee Management Portal's location filter as an example.
  3. Describe a scenario where linkedSignal() is more appropriate than plain computed().
  4. How would you structure a signal-based store service to prevent external components from mutating internal state directly?
  5. What are the tradeoffs of running an Angular application zoneless, relying purely on Signals for change detection?

Scenario Based

  1. In the Employee Management Portal, the HR executive selects "Finance" as the department in the Add Employee form, and the location dropdown auto-suggests "Pune." They then manually change the location to "Chennai." If they change the department to "Engineering" next, should the location reset? Explain using linkedSignal() semantics.
  2. A junior developer on your team writes this.employees().push(newEmployee) and complains the UI does not update. Diagnose the bug and explain the fix.
  3. Design the signal-based state needed to support: a search box, a department filter, a location filter, and a computed "no results found" message — all combined.
  4. Two sibling components — an employee list and an attendance widget — need to share the "currently selected employee." Design the service that connects them using Signals.
  5. Your team lead asks you to justify moving a feature from RxJS BehaviorSubject-based state to Signals. Write the technical justification you would present.

Best Practices

  • Keep signal state immutable — always produce new object/array references on update.
  • Prefer many small, focused signals over one large state blob, unless building a deliberate store.
  • Derive, don't duplicate — use computed() instead of manually keeping a second signal in sync.
  • Use effect() only for side effects (logging, persistence, DOM APIs) — never to compute a value used elsewhere.
  • Expose service state as readonly via .asReadonly(); keep the writable signal private.
  • Use input.required<T>() for any input the component cannot function without.
  • Prefer resource() over manual fetch/subscribe combinations for signal-driven async data.
  • Track @for loops with a stable identifier (track emp.id), never $index, when the list can reorder or filter.
  • Use linkedSignal() when a value should default from another signal but remain independently overridable.
  • Combine RxJS and Signals deliberately: RxJS for complex async orchestration, Signals for holding and deriving state.

Anti-Patterns

  • Mutating in place: this.employees().push(x) — silently breaks reactivity.
  • Signal soup: dozens of unrelated signals with no grouping, making a component's state impossible to reason about.
  • Manually syncing derived state: maintaining a second signal for a value that could be a computed().
  • Using effect() as a computed: assigning to another signal inside an effect purely to "compute" a value, instead of using computed().
  • Global signal for everything: promoting every piece of state, even purely local UI state like a tooltip's visibility, into a global store.
  • Deeply nested mutable objects: signals holding deeply nested structures without a clear immutable update strategy, leading to bugs when only a shallow copy is made.
  • Ignoring resource() cancellation semantics: manually racing fetches with booleans and flags instead of relying on resource()'s built-in stale-response discarding.

Common Mistakes

  • Forgetting the parentheses when reading a signal in a template: {{ employees }} instead of {{ employees() }}.
  • Declaring a signal inside a method body instead of as a class field, resetting its identity on every call.
  • Expecting computed() to re-run on a schedule (like setInterval) rather than only when a dependency changes.
  • Reading a signal inside an asynchronous callback (setTimeout, .then()) inside a computed() and expecting it to be tracked — it will not be.
  • Treating effect() as a place to fetch data tied to a signal, instead of using resource().
  • Forgetting .asReadonly() on a service's public signal, allowing any component to call .set() on shared state from anywhere.

Cheatsheet

// Create
const count = signal(0);

// Read
count();

// Set
count.set(5);

// Update
count.update(c => c + 1);

// Computed (read-only, derived, cached, lazy)
const doubled = computed(() => count() * 2);

// Effect (side effects only, auto-cleanup on destroy)
effect(() => console.log(count()));

// Effect with manual cleanup
effect((onCleanup) => {
  const id = setInterval(() => console.log(count()), 1000);
  onCleanup(() => clearInterval(id));
});

// Input signal
name = input<string>('default');
requiredName = input.required<string>();

// Output signal
saved = output<Employee>();
this.saved.emit(employee);

// Linked signal (resettable default, overridable)
const band = linkedSignal(() => defaultBandFor(department()));

// Resource (async tied to a signal)
const data = resource({
  request: () => someSignal(),
  loader: async ({ request }) => fetch(`/api/x/${request}`).then(r => r.json())
});
data.value();
data.isLoading();
data.error();
data.reload();

// Read-only view of a writable signal (for services)
private _state = signal(initial);
readonly state = this._state.asReadonly();

// Bridge RxJS Observable -> Signal
const value = toSignal(someObservable$, { initialValue: fallback });

Quiz

Test your understanding before moving on to writing code. Answers are intentionally not provided — discuss them with a peer, a mentor, or in a study group, and verify against this guide.

  1. What will the template show if you write {{ employees }} instead of {{ employees() }}?
  2. Why does this.employees().push(newEmployee) fail to trigger a UI update?
  3. A computed() signal is read three times in a row with no dependency change in between. How many times does its function body actually execute?
  4. In the Add Employee form, department changes from Finance to Engineering. What happens to a linkedSignal()-based location suggestion, if the HR executive had manually overridden it after the last department change?
  5. Why can't a signal read inside a setTimeout callback, inside a computed(), be tracked as a dependency?
  6. What is the correct way to expose a service's internal signal to the rest of the app so that only the service itself can update it?
  7. Name one scenario in the Employee Management Portal where RxJS is clearly the better tool than a plain signal, and explain why.
  8. What does resource() do automatically when its request signal changes before the previous request has resolved?
  9. Why is input.required<T>() considered safer than a plain optional @Input() for the employee field on EmployeeCardComponent?
  10. What is the key difference in intent between effect() and computed(), even though both "react" to signal changes?

Mini Assignments

Hands-on tasks to apply what you have learned. Each builds directly on the Employee Management Portal used throughout this guide.

  1. Add a department filter. Add a selectedDepartment signal and a computed() that filters employees by both selectedLocation and selectedDepartment together.

  2. Build the "give raise" feature. Add a button on each EmployeeCardComponent that emits an output() event with the employee's id and a fixed raise amount (say, ₹5,000). Handle it in the parent using .update() on the employees signal, with an immutable map.

  3. Add a "highest paid employee" computed signal. Write a computed() that returns the single highest-paid employee in the currently filtered list, and display their name and salary at the top of the page. Handle the empty-list case gracefully.

  4. Persist the last-used location filter. Use an effect() to save selectedLocation to localStorage on every change, and read it back as the initial value when the component is created.

  5. Implement the salary-band linkedSignal(). In the Add Employee form, implement selectedBand as a linkedSignal() derived from department, and verify (by testing manually in the browser) that a manual override is discarded when the department changes again.

  6. Build a mini attendance widget using resource(). Given a selectedEmployeeId signal, use resource() to fetch a mock attendance summary (you can hardcode a fake async function returning sample data for Mahesh Deshpande or Kamlesh Verma), and show loading/error/success states in the template.

  7. Refactor to a signal-based store. Move employees, selectedLocation, and filteredEmployees out of EmployeeListComponent and into an injectable EmployeeStore service with providedIn: 'root', exposing read-only signals and mutation methods (addEmployee, removeEmployee, giveRaise).

  8. Debounced search with RxJS + Signals. Implement the search box using a Subject, debounceTime(300), distinctUntilChanged(), and toSignal(), then derive searchedEmployees as a computed() on top of the debounced signal.


Summary

Angular Signals give you a reactive primitive that is synchronous to read, automatically tracked for dependencies, cached when derived, and integrated cleanly with modern Angular's standalone components, input()/output(), and OnPush change detection.

Across this guide, the Employee Management Portal grew from a bare Angular CLI scaffold into an application with:

  • Signal-based employee state (signal(), computed())
  • Reactive side effects (effect(), with cleanup and injection-context awareness)
  • Signal-based parent-child communication (input(), output())
  • Resettable, overridable derived defaults (linkedSignal())
  • Signal-driven async data fetching (resource())
  • A signal-based store service as a single source of truth
  • A clear understanding of where RxJS still belongs alongside Signals
  • A feature-based, enterprise-ready folder structure

The core idea to carry forward into any Angular project — whether it's an Employee Management Portal for NimbusSoft Technologies, a ticketing system for Indian Railways, or a policy dashboard for LIC — is the same: state lives in signals, derived values live in computed signals, side effects live in effects, and everything else follows from that one distinction.

Releases

Packages

Contributors