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.
- Overview
- Why Angular Signals?
- Problems with Traditional State Management
- Evolution: Zone.js → Change Detection → RxJS → Signals
- Learning Roadmap
- Project Overview
- Folder Structure
- Creating the Angular Project
- Understanding Reactivity
- Signals Fundamentals
- Computed Signals
- Effects
- Writable Signals
- Input Signals
- Output Signals
- Linked Signals
- Resource API
- Forms with Signals
- HTTP with Signals
- Component Communication
- State Management
- Signals vs RxJS
- Performance
- Enterprise Folder Structure
- Real World Architecture
- Flow Diagrams
- Complete Learning Roadmap
- Interview Questions
- Best Practices
- Anti-Patterns
- Common Mistakes
- Cheatsheet
- Quiz
- Mini Assignments
- Summary
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
Observablestreams, 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 touseState,computed()as similar touseMemo, andeffect()as similar touseEffect— but with automatic dependency tracking. You never declare a dependency array. Angular figures it out by watching which signals you read inside the function.
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.
| 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. |
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.
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.
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.
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?"
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.
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.
Understanding why Signals exist requires understanding the road Angular walked to get here.
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]
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
asyncpipe, to avoid leaks. - A gap between "reactive data" (Observables) and "template-bindable data" (plain properties), bridged awkwardly by the
asyncpipe or manual subscription-to-property assignment.
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
| 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 (
BehaviorSubjectused as a store). RxJS remains the right tool for complex async pipelines — merging streams, debouncing search input, retry logic with backoff, and so on.
This is the order in which this README teaches Signals, and the order in which the Employee Management Portal will grow:
- Scaffold the Angular project and understand its structure.
- Understand why reactivity matters before touching Signal syntax.
- Learn
signal()— create, read, update, mutate. - Learn
computed()— derive values from signals. - Learn
effect()— react to signal changes with side effects. - Deep dive into writable signal update patterns.
- Learn signal-based
input()for parent-to-child communication. - Learn signal-based
output()for child-to-parent communication. - Learn
linkedSignal()for state that resets/derives conditionally. - Learn
resource()for async data fetching tied to signals. - Combine Signals with Reactive Forms.
- Fetch employee data over HTTP using signal-friendly patterns.
- Study component communication patterns end-to-end.
- Build application-wide state using signal-based services.
- Compare Signals and RxJS directly, and learn when to use each.
- Study performance characteristics and best practices.
- Study an enterprise-grade folder structure for a real project.
- Study the full application architecture with diagrams.
- Review interview questions, best practices, and a cheatsheet.
Application name: Employee Management Portal Company: NimbusSoft Technologies (fictional IT company) Offices: Pune, Chennai Primary users: HR executives and team managers
| 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 |
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' },
];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.
- Node.js LTS installed
- Angular CLI installed globally
npm install -g @angular/cling new angular-signals-reactive-state --standalone --style=css --routing
cd angular-signals-reactive-stateAngular 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.
ng serve -oThis opens the app at http://localhost:4200.
ng generate component features/employee-list --standaloneNote Since Angular v17+,
--standaloneis the default when generating components in a standalone project, so you may omit the flag. It is shown here for clarity.
Before writing a single signal(), you need three mental models solid: reactive programming, push vs pull, and state vs derived state.
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.
- 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 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);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().
A signal is a wrapper around a value that:
- Lets you read the current value by calling it as a function:
mySignal(). - Lets you write a new value:
mySignal.set(newValue)ormySignal.update(fn). - Notifies any
computed(),effect(), or template binding that read it, whenever the value changes.
Because it turns "a value that changes" into "a value whose changes are trackable," without you writing any explicit subscription code.
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.
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
import { signal } from '@angular/core';
export class EmployeeListComponent {
employees = signal<Employee[]>(SAMPLE_EMPLOYEES);
searchTerm = signal<string>('');
selectedLocation = signal<'Pune' | 'Chennai' | 'All'>('All');
}Signals are read by calling them as functions:
console.log(this.employees().length); // 6In templates:
<p>Total employees: {{ employees().length }}</p>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]);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)
);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 }
}));
}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([]);- 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, notemployeeData1. - Prefer
computed()over storing and manually syncing derived values.
| 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 } |
- 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.
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);
});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
setTimeoutor after anawaitinside a computed function will not be tracked.
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.
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 againComputed 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]
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>
}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()}`);
});
}
}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})`;
});
}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));
});
}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 });
}
}| 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) |
// 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}`);
});
}A writable signal is what signal() creates — as opposed to computed(), which produces a read-only signal.
.update() receives the current value and returns the new value:
salaryHike = signal(0);
salaryHike.update(current => current + 5000);.set() replaces the value directly:
selectedLocation.set('Chennai');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
)
);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.
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'" />
}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.
name = input('', { alias: 'employeeName' });
isVip = input(false, { transform: (v: string | boolean) => !!v });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.
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);
}
}| 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) |
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
departmentchanges to'Finance',selectedBandautomatically resets to'Band 2', discarding the manual override, because its source changed.
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'
);
}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() 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" />
}
}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'.
<button (click)="employeesResource.reload()">Refresh</button>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.
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.validis a plain getter, not a signal, so wrapping it incomputed()alone will not make it reactive. Prefer deriving validity fromtoSignal(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');<input
name="employeeName"
[ngModel]="name()"
(ngModelChange)="name.set($event)"
required
/>name = signal('');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>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[] }
);
}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[]>;
}
});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');
}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;
}
});This chapter ties input(), output(), and shared services together in one worked example: the Employee List → Employee Card → Edit Form flow.
<!-- employee-list.component.html -->
@for (emp of searchedEmployees(); track emp.id) {
<app-employee-card [employee]="emp" (edit)="startEdit($event)" (delete)="remove($event)" />
}// employee-card.component.ts
edit = output<Employee>();
delete = output<number>();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.
flowchart TB
subgraph AppComponent
EL[EmployeeListComponent]
AW[AttendanceWidgetComponent]
end
SVC[(EmployeeSelectionService signal)]
EL -- selects employee --> SVC
SVC -- selectedEmployeeId signal --> AW
@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).
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 });
}| 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.
searchTermin 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.
| 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 |
- 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()).
- 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,combineLatestin combination.
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));
});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).
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."
effect()s created in a constructor are automatically cleaned up when the component/service is destroyed — no manualngOnDestroybookkeeping.- 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.
- 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 acomputed()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(nottrack $index) in@forloops so Angular can correctly reuse DOM nodes when the underlying array changes.
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.
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
| 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() |
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
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
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)
flowchart LR
Parent[EmployeeListComponent] -- input: employee --> Child[EmployeeCardComponent]
Child -- output: edit, delete --> Parent
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
flowchart TB
Form[AddEmployeeFormComponent] -- addEmployee(e) --> Store[EmployeeStore]
List[EmployeeListComponent] -- reads employees, filteredEmployees --> Store
Card[EmployeeCardComponent] -- giveRaise(id, amount) --> Store
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()andinput.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
- What is a Signal in Angular, and how do you read its value?
- What is the difference between
signal()andcomputed()? - Why do you call a signal as a function, e.g.
employees(), instead of accessing it as a property? - What happens if you mutate an array returned by a signal directly instead of using
.update()? - What is the difference between
.set()and.update()?
- Explain how Angular tracks dependencies for a
computed()signal without an explicit dependency array. - Why is a
computed()signal described as "lazily evaluated and cached"? Give an example where this matters. - What is the purpose of the cleanup function inside
effect(), and when would you use it? - Why might you get an "injection context" error when calling
effect(), and how do you fix it? - How does
input.required<T>()improve on the traditional@Input()decorator?
- Compare Signals and RxJS in terms of how they handle multiple values over time versus a single current value.
- Explain how
resource()handles request cancellation when its source signal changes rapidly, using the Employee Management Portal's location filter as an example. - Describe a scenario where
linkedSignal()is more appropriate than plaincomputed(). - How would you structure a signal-based store service to prevent external components from mutating internal state directly?
- What are the tradeoffs of running an Angular application zoneless, relying purely on Signals for change detection?
- 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. - 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. - 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.
- 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.
- Your team lead asks you to justify moving a feature from RxJS
BehaviorSubject-based state to Signals. Write the technical justification you would present.
- 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
readonlyvia.asReadonly(); keep the writable signal private. - Use
input.required<T>()for any input the component cannot function without. - Prefer
resource()over manualfetch/subscribecombinations for signal-driven async data. - Track
@forloops 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.
- 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 usingcomputed(). - 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 onresource()'s built-in stale-response discarding.
- 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 (likesetInterval) rather than only when a dependency changes. - Reading a signal inside an asynchronous callback (
setTimeout,.then()) inside acomputed()and expecting it to be tracked — it will not be. - Treating
effect()as a place to fetch data tied to a signal, instead of usingresource(). - Forgetting
.asReadonly()on a service's public signal, allowing any component to call.set()on shared state from anywhere.
// 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 });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.
- What will the template show if you write
{{ employees }}instead of{{ employees() }}? - Why does
this.employees().push(newEmployee)fail to trigger a UI update? - 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? - In the Add Employee form, department changes from
FinancetoEngineering. What happens to alinkedSignal()-based location suggestion, if the HR executive had manually overridden it after the last department change? - Why can't a signal read inside a
setTimeoutcallback, inside acomputed(), be tracked as a dependency? - 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?
- Name one scenario in the Employee Management Portal where RxJS is clearly the better tool than a plain signal, and explain why.
- What does
resource()do automatically when its request signal changes before the previous request has resolved? - Why is
input.required<T>()considered safer than a plain optional@Input()for theemployeefield onEmployeeCardComponent? - What is the key difference in intent between
effect()andcomputed(), even though both "react" to signal changes?
Hands-on tasks to apply what you have learned. Each builds directly on the Employee Management Portal used throughout this guide.
-
Add a department filter. Add a
selectedDepartmentsignal and acomputed()that filtersemployeesby bothselectedLocationandselectedDepartmenttogether. -
Build the "give raise" feature. Add a button on each
EmployeeCardComponentthat emits anoutput()event with the employee's id and a fixed raise amount (say, ₹5,000). Handle it in the parent using.update()on theemployeessignal, with an immutable map. -
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. -
Persist the last-used location filter. Use an
effect()to saveselectedLocationtolocalStorageon every change, and read it back as the initial value when the component is created. -
Implement the salary-band
linkedSignal(). In the Add Employee form, implementselectedBandas alinkedSignal()derived fromdepartment, and verify (by testing manually in the browser) that a manual override is discarded when the department changes again. -
Build a mini attendance widget using
resource(). Given aselectedEmployeeIdsignal, useresource()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. -
Refactor to a signal-based store. Move
employees,selectedLocation, andfilteredEmployeesout ofEmployeeListComponentand into an injectableEmployeeStoreservice withprovidedIn: 'root', exposing read-only signals and mutation methods (addEmployee,removeEmployee,giveRaise). -
Debounced search with RxJS + Signals. Implement the search box using a
Subject,debounceTime(300),distinctUntilChanged(), andtoSignal(), then derivesearchedEmployeesas acomputed()on top of the debounced signal.
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.