Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 1: Introduction to Angular

Angular is one of the most widely used web frameworks in the world. It powers applications from small startups to global enterprises, offering a complete solution for building scalable, maintainable, and modern web apps.

In this chapter, we’ll introduce Angular, explore its history, compare it with other frameworks, and look at real-world use cases.


1.1 Why Angular?

Angular is more than just a UI library — it’s a comprehensive application framework.

Key reasons to choose Angular:

  • Batteries included: Comes with routing, forms, HTTP, and state management tools out of the box.
  • TypeScript first: Built on TypeScript, offering strong typing, tooling, and maintainability.
  • Enterprise-ready: Designed for large-scale apps with complex requirements.
  • Consistency: Provides a unified way to structure apps, reducing “decision fatigue.”
  • Long-term support: Backed by Google, with regular updates and strong community adoption.

👉 In short: Angular is ideal when you want a full framework with conventions, not just a rendering library.


1.2 History and Evolution

Angular has gone through major transformations.

  1. AngularJS (2010)

    • Based on JavaScript.
    • Introduced two-way data binding and declarative templates.
    • Quickly gained popularity but struggled with performance and complexity.
  2. Angular 2+ (2016 onwards)

    • Complete rewrite in TypeScript.
    • Component-based architecture.
    • Improved performance with unidirectional change detection.
    • Modular design for scalability.
  3. Modern Angular (v14–v17)

    • Standalone components (no need for NgModules).
    • Signals for fine-grained reactivity.
    • New template syntax: @if, @for, @defer.
    • Stronger developer ergonomics and simpler architecture.

👉 Angular has evolved from a pioneering framework into a modern, efficient, and future-proof platform.


1.3 Angular vs React vs Vue

FeatureAngularReactVue
TypeFramework (full solution)Library (UI only)Framework-lite
LanguageTypeScript (first-class)JavaScript (with TS support)JavaScript (TS optional)
State ManagementServices, Signals, NgRxExternal libs (Redux, Zustand)Vuex / Pinia
RoutingBuilt-in @angular/routerExternal (React Router)Built-in (Vue Router)
Learning CurveSteeper (full ecosystem)Medium (UI only, many choices)Easy (progressive)
Best ForLarge, enterprise appsStartups, highly flexible appsSmall-to-medium apps

✅ Angular: great when you want everything in one package.
✅ React: flexible, lightweight, lots of choices.
✅ Vue: approachable and beginner-friendly.


1.4 Real-World Use Cases

Angular is used by some of the world’s biggest companies and organizations:

  • Google (internal apps + Google Cloud Console).
  • Microsoft (Office 365 Admin tools).
  • Deutsche Bank (trading applications).
  • Upwork (freelance marketplace).
  • Forbes (content-heavy media site).

Common scenarios where Angular excels:

  • Enterprise dashboards with complex data.
  • E-commerce platforms needing SEO and scalability.
  • Content management systems with modular features.
  • Large-scale SPAs with long-term maintenance needs.

1.5 Summary

  • Angular is a full-fledged framework for building web apps.
  • It evolved from AngularJS into a modern, TypeScript-based platform.
  • Compared with React and Vue, Angular is more opinionated and enterprise-focused.
  • Real-world adoption proves its reliability for complex, large-scale projects.

Chapter 2: Project Setup

Before we dive into components, directives, and state management, we need to set up an Angular project. Angular offers a powerful CLI (Command Line Interface) that simplifies installation, scaffolding, and development.


2.1 Prerequisites

To follow along, ensure you have:

  • Node.js (LTS version recommended, e.g., v18+).
  • npm (comes with Node) or yarn/pnpm as a package manager.
  • A code editor (VS Code is highly recommended).

Check your installation:

node -v
npm -v

2.2 Installing Angular CLI

The Angular CLI makes it easy to create and manage projects.

Install globally:

npm install -g @angular/cli

Verify installation:

ng version

2.3 Creating a New Project

Generate a new Angular project using:

ng new my-app

CLI prompts:

  • Would you like to add Angular routing? → Yes (so we have navigation support).
  • Which stylesheet format would you like to use? → Choose (CSS, SCSS, or LESS).

Navigate to the project folder:

cd my-app

Start the development server:

ng serve

Open http://localhost:4200 to see your app running. 🎉


2.4 Project Structure

The generated app contains several files and folders. Let’s break them down:

my-app/
 ├── src/
 │   ├── app/
 │   │   ├── app.component.ts      # Root component logic
 │   │   ├── app.component.html    # Root component template
 │   │   ├── app.component.css     # Styles for root component
 │   │   └── app.routes.ts         # Routes definition (standalone-first)
 │   ├── assets/                   # Static assets (images, icons)
 │   ├── index.html                # Entry point HTML
 │   ├── main.ts                   # Bootstraps Angular app
 │   └── styles.css                # Global styles
 ├── angular.json                  # CLI configuration
 ├── package.json                  # Dependencies and scripts
 ├── tsconfig.json                 # TypeScript configuration
 └── README.md                     # Project instructions

Key files:

  • main.ts: Entry point that bootstraps the Angular app.
  • app.component.ts: Root component.
  • app.routes.ts: Defines routing (standalone-first).
  • angular.json: CLI build and serve config.

2.5 Bootstrapping with Standalone Components

By default (Angular 17+), projects use standalone-first architecture.

main.ts:

import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
  providers: [provideRouter(routes)]
});

👉 No AppModule is required — the app starts with AppComponent.


2.6 Development Workflow

Common CLI commands:

  • ng serve → Run dev server.
  • ng generate component hello → Generate a component.
  • ng generate service auth → Generate a service.
  • ng test → Run unit tests.
  • ng build → Build for production.

2.7 Summary

  • Installed Angular CLI for scaffolding and management.
  • Created a new Angular project with ng new.
  • Explored the project structure and key files.
  • Learned that Angular 17+ uses standalone-first bootstrapping.
  • Got familiar with common development commands.

In the next chapter, we’ll dive into Components, the building blocks of Angular applications, learning how to create, structure, and reuse them.

Chapter 3: TypeScript Primer

Angular is written in TypeScript, a superset of JavaScript that adds static typing and modern features. If you’re new to TypeScript, this chapter will give you just enough background to be productive in Angular.


3.1 What is TypeScript?

  • TypeScript = JavaScript + Types.
  • Compiles down to plain JavaScript (runs everywhere).
  • Adds features like interfaces, generics, enums, decorators.
  • Helps prevent bugs by catching errors at compile-time.

3.2 Basic Types

let isLoggedIn: boolean = true;
let age: number = 25;
let name: string = 'Alice';
let tags: string[] = ['angular', 'typescript'];
let user: [string, number] = ['Bob', 33]; // tuple

3.3 Functions

function add(a: number, b: number): number {
  return a + b;
}

const multiply = (a: number, b: number): number => a * b;
  • Return type can be inferred (often optional).
  • Parameters can be optional or have default values:
function greet(name: string = 'Guest'): string {
  return `Hello, ${name}`;
}

3.4 Interfaces

Interfaces define object shapes.

interface User {
  id: number;
  name: string;
  email?: string; // optional property
}

const user: User = { id: 1, name: 'Alice' };

3.5 Classes

Angular uses classes everywhere (components, services, etc.).

class Person {
  constructor(public name: string, private age: number) {}

  greet(): string {
    return `Hi, I'm ${this.name}`;
  }
}

const bob = new Person('Bob', 25);
console.log(bob.greet());

3.6 Generics

Generics allow reusable, type-safe code.

function identity<T>(value: T): T {
  return value;
}

let num = identity<number>(42); // num: number
let str = identity('Angular');  // str: string

3.7 Enums

Enums define a set of named constants.

enum Role {
  Admin,
  User,
  Guest
}

const currentRole: Role = Role.Admin;

3.8 Type Aliases & Union Types

type ID = number | string;

let userId: ID = 42;
userId = 'abc123'; // valid

3.9 Decorators

Angular uses decorators heavily (@Component, @Injectable, etc.).
A decorator adds metadata to classes.

function Log(constructor: Function) {
  console.log('Class decorated:', constructor.name);
}

@Log
class Demo {}
// Output: "Class decorated: Demo"

Great idea 🙌 — null safety and union types are crucial in TypeScript, especially since Angular templates often run into null | undefined values. Let’s expand the TypeScript Primer with a dedicated section on null safety and union types.


3.13 Null Safety and Strict Mode

By default, TypeScript in strict mode prevents you from using null or undefined unless explicitly allowed. This eliminates a whole class of runtime errors.

let name: string = 'Alice';
name = null;      // ❌ Error in strict mode
name = undefined; // ❌ Error

To allow null/undefined, you must explicitly declare it with union types:

let name: string | null = null;
let age: number | undefined = undefined;

Optional Chaining

Instead of manually checking for null, you can use ?. (optional chaining).

interface User {
  profile?: { email?: string };
}

const user: User = {};
console.log(user.profile?.email ?? 'No email');
// Output: "No email"
  • ?. → Safely accesses a property if the object exists.
  • ?? → Nullish coalescing operator, gives a default if null or undefined.

3.11 Union Types in Depth

Union types let a variable hold multiple possible types.

let id: string | number;

id = 42;      // ✅ ok
id = 'abc';   // ✅ ok
id = true;    // ❌ not allowed

Narrowing Union Types

You can narrow down a union using type guards.

function printId(id: string | number) {
  if (typeof id === 'string') {
    console.log(id.toUpperCase()); // string methods
  } else {
    console.log(id.toFixed(2));    // number methods
  }
}

Union with Interfaces

interface Dog { bark(): void; }
interface Cat { meow(): void; }

function interact(pet: Dog | Cat) {
  if ('bark' in pet) {
    pet.bark();
  } else {
    pet.meow();
  }
}

Literal Types with Union

Union types can be combined with literal values to restrict choices.

type Direction = 'up' | 'down' | 'left' | 'right';

function move(dir: Direction) {
  console.log(`Moving ${dir}`);
}

move('up');    // ✅ ok
move('north'); // ❌ Error

Perfect catch ✅ — the Elvis operator (?.) is something Angular developers see very often, and it ties directly into the null safety and optional chaining topic we just added. Let’s extend the TypeScript Primer with this, but also show how it works in Angular templates (where it first became popular).


3.12 The Elvis Operator (?.)

The Elvis operator (also known as the safe navigation operator) is shorthand for optional chaining in TypeScript and Angular. It allows you to safely access properties of an object that might be null or undefined, without throwing an error.


In TypeScript

interface User {
  profile?: {
    email?: string;
  };
}

const user: User = {};
console.log(user.profile?.email); // undefined, no runtime error

Without ?., this would throw:

console.log(user.profile.email); // ❌ Cannot read property 'email' of undefined

In Angular Templates

Angular templates supported the Elvis operator (?.) even before TypeScript added optional chaining.

<p>Email: {{ user?.profile?.email }}</p>
  • If user or profile is null/undefined → prints nothing.
  • If everything exists → prints the email.

✅ Prevents “Cannot read property of undefined” runtime errors in templates.


With Safe Method Calls

You can also call methods safely:

user?.sendMessage?.('Hello');

If user or sendMessage is missing → the call is skipped.


With Arrays

let items: string[] | null = null;

console.log(items?.length); // undefined

Combining with Nullish Coalescing (??)

For defaults:

<p>Email: {{ user?.profile?.email ?? 'No email provided' }}</p>

Perfect ✅ — let’s extend the TypeScript Primer with a mini guide on how TypeScript features show up inside Angular templates and code. This bridges the gap between theory and real Angular usage.


3.12 TypeScript in Angular Context

While TypeScript is the foundation of Angular, not all features behave exactly the same in Angular templates. Here are some Angular-specific tips and “gotchas” you’ll encounter often.


3.12.1 The Elvis Operator (?.) in Templates

Already discussed, but let’s emphasize Angular usage:

<p>Email: {{ user?.profile?.email }}</p>
  • Prevents runtime errors when user or profile is null.
  • Very common when binding API-driven data that may not exist yet.

3.12.2 Non-Null Assertion Operator (!)

Sometimes you know a value won’t be null, but TypeScript can’t infer it. In Angular, this often happens with @Input() properties or template references.

@Component({
  selector: 'app-profile',
  standalone: true,
  template: `<p>{{ user!.name }}</p>`
})
export class ProfileComponent {
  @Input() user!: { name: string }; // “trust me, this will be set”
}

✅ Tells TypeScript: this will not be null.
⚠️ Use carefully — you can cause runtime errors if you’re wrong.


3.12.3 Type Assertions (as) in Templates

Angular templates allow as casting for better type narrowing:

<div *ngIf="user as u">
  <p>Hello {{ u.name }}</p>
</div>

Here:

  • user is checked for truthy value.
  • If true, it’s assigned to u with narrowed type.

This avoids repeating user?.name.


3.12.4 Strict Template Checking

With Angular’s strictTemplates option enabled (angularCompilerOptions.strictTemplates: true in tsconfig.json), the compiler will:

  • Catch invalid property bindings.
  • Ensure correct types in event handlers.
  • Detect missing async pipe unwraps.

Example:

<p>{{ user.age.toUpperCase() }}</p>

❌ Error: age is a number, not a string.

Strict templates = safer apps.


3.12.5 Async Pipe with Type Safety

Angular templates use the async pipe to unwrap Observables and Promises.

<p *ngIf="user$ | async as user">
  Hello {{ user.name }}
</p>
  • The as user syntax narrows the type inside the block.
  • No need for Elvis operator inside the block.

3.12.6 Angular Inputs and Null Safety

Inputs often need explicit null handling:

@Component({
  selector: 'app-avatar',
  standalone: true,
  template: `
    <img [src]="url ?? 'default.png'" />
  `
})
export class AvatarComponent {
  @Input() url: string | null = null;
}

Using ?? ensures there’s always a fallback.


3.12.7 Angular with Union Types

Union types model flexible inputs:

@Component({
  selector: 'app-status',
  standalone: true,
  template: `<p>Status: {{ status }}</p>`
})
export class StatusComponent {
  @Input() status: 'online' | 'offline' | 'busy' = 'offline';
}

✅ Type-safe: only accepts allowed values.
❌ Binding something else (<app-status status="sleeping">) → compile error.


3.13 Putting It Together

Angular templates are TypeScript-aware, but they extend it with:

  • ?. (safe navigation / Elvis operator).
  • ! (non-null assertion for inputs and bindings).
  • as (template type narrowing).
  • async pipe for Observables/Promises.
  • Strict template checking to enforce correctness.

3.14 Summary

  • TypeScript prevents null and undefined bugs with strict null checks.
  • Use string | null or number | undefined to allow nullable types.
  • Use optional chaining (?.) and nullish coalescing (??) for safety.
  • Union types let you model flexible values (string | number).
  • Type guards (typeof, in, instanceof) help narrow union types.
  • Literal unions let you model finite sets of allowed values.

👉 These features are heavily used in Angular:

  • Template bindings often deal with null | undefined.
  • Inputs and services may accept multiple types.
  • Strong typing makes your app safer and easier to maintain.

Chapter 4: Components and Templates

Angular applications are built out of components. If you imagine an Angular app as a house, components are the rooms: each with a specific purpose, walls that separate it from the rest, and an entrance that lets you interact with it. Together, they form the whole structure.

This chapter will walk you through everything you need to know about components — how they are created, structured, and how they interact with templates. By the end, you’ll have built a solid foundation for building user interfaces in Angular.


4.1 What is a Component?

A component is the basic building block of Angular applications. It controls a portion of the screen — called a view. Every Angular component has three main parts:

  1. Class (TypeScript) – defines the behavior and data.
  2. Template (HTML) – defines the UI layout.
  3. Metadata (Decorator) – tells Angular how to connect the class and the template.

At runtime, Angular takes your component class, combines it with the template, and renders it into the browser DOM.


4.2 Creating a Component

The easiest way to create a new component is by using the Angular CLI:

ng generate component hello-world
# or shorthand:
ng g c hello-world

This command will create four files in a new folder hello-world/:

hello-world/
  hello-world.component.ts      # Component class + metadata
  hello-world.component.html    # Template
  hello-world.component.css     # Styles
  hello-world.component.spec.ts # Unit tests

Let’s open hello-world.component.ts:

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

@Component({
  selector: 'app-hello-world',
  templateUrl: './hello-world.component.html',
  styleUrls: ['./hello-world.component.css']
})
export class HelloWorldComponent {
  message: string = 'Hello, Angular!';
}
  • @Component is a decorator that marks the class as an Angular component.
  • selector is the HTML tag you use to render the component.
  • templateUrl points to the component’s HTML file.
  • styleUrls points to the CSS file.

In hello-world.component.html, you might write:

<h1>{{ message }}</h1>

And finally, in app.component.html, include your component:

<app-hello-world></app-hello-world>

4.3 Template Syntax

Angular templates are more than static HTML. They include a set of powerful features:


4.3.1 Interpolation

Use {{ ... }} to display component data.

TypeScript (Component):

export class HelloWorldComponent {
  message: string = 'Angular is awesome!';
}

Template:

<p>{{ message }}</p>

4.3.2 Property Binding

Bind element properties to component values using [ ].

TypeScript (Component):

export class ProfileComponent {
  profilePictureUrl: string = 'https://example.com/avatar.png';
}

Template:

<img [src]="profilePictureUrl" alt="User profile picture">

4.3.3 Event Binding

Bind DOM events to component methods using ( ).

TypeScript (Component):

export class ButtonComponent {
  sayHello() {
    alert('Hello from Angular!');
  }
}

Template:

<button (click)="sayHello()">Click me</button>

4.3.4 Two-Way Binding

Use [(ngModel)] for two-way data binding (requires FormsModule).

TypeScript (Component):

export class UserFormComponent {
  username: string = '';
}

Template:

<input [(ngModel)]="username" placeholder="Enter your name">
<p>You typed: {{ username }}</p>

4.4 Lifecycle Hooks

Angular components have a lifecycle — from creation to destruction. You can hook into this lifecycle using special methods:

  • ngOnInit() – runs once after component initialization.
  • ngOnChanges() – runs when input properties change.
  • ngOnDestroy() – runs just before the component is removed.

Example:

export class HelloWorldComponent implements OnInit, OnDestroy {
  ngOnInit() {
    console.log('Component initialized');
  }

  ngOnDestroy() {
    console.log('Component destroyed');
  }
}

4.5 Component Communication

Components rarely live in isolation. They need to talk to each other.

4.5.1 Input Properties

Pass data from a parent component into a child component:

export class ChildComponent {
  @Input() title!: string;
}

Template usage:

<app-child [title]="'Dashboard'"></app-child>

4.5.2 Output Events

Send data from a child component to its parent:

export class ChildComponent {
  @Output() clicked = new EventEmitter<string>();

  handleClick() {
    this.clicked.emit('Child button clicked!');
  }
}

Template:

<button (click)="handleClick()">Click me</button>

Parent usage:

<app-child (clicked)="onChildClicked($event)"></app-child>

4.6 Styling Components

Angular components have their own style files. By default, styles are scoped to the component (thanks to View Encapsulation).

h1 {
  color: blue;
}

This CSS applies only to the component’s template, not globally. You can override this behavior by changing encapsulation in the decorator.


4.7 Best Practices

  • Keep components small and focused – one responsibility per component.
  • Use smart (container) and dumb (presentational) components.
  • Avoid deeply nested components; favor flat hierarchies.
  • Name selectors consistently (e.g., always prefix with app-).

4.8 Summary

In this chapter, you learned that:

  • A component is the fundamental building block of Angular apps.
  • Components consist of a class, template, and metadata.
  • Templates use Angular’s powerful binding syntax.
  • Lifecycle hooks let you react to changes over time.
  • Components communicate via @Input and @Output.

Chapter 5: Standalone vs. NgModules

When Angular was first released in 2016, NgModules were at the heart of every application. They provided a way to group components, directives, pipes, and services together. But as applications grew, many developers found the module system verbose and confusing for newcomers.

With Angular v14 (2022), the framework introduced standalone components — a simpler, more direct way to build apps without NgModules. Since then, standalone APIs have become a cornerstone of Angular’s modernization.

In this chapter, we’ll explore what standalone components are, how they compare to NgModules, and when you should use one over the other.


5.1 A Quick Refresher: What Are NgModules?

Traditionally, Angular apps required at least one root module (usually AppModule) and often many feature modules.

Example of a basic NgModule:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { HelloWorldComponent } from './hello-world/hello-world.component';

@NgModule({
  declarations: [AppComponent, HelloWorldComponent],
  imports: [BrowserModule, FormsModule],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Here:

  • declarations register components, directives, and pipes.
  • imports bring in other modules (like FormsModule).
  • bootstrap defines the entry component.

This pattern worked, but it also created confusion: Do I declare this in the module or import it? Where does a service go?


5.2 What Are Standalone Components?

A standalone component removes the need for NgModules. Instead of declaring a component in a module, you mark it as standalone: true.

Example:

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

@Component({
  selector: 'app-hello-world',
  standalone: true,
  template: `<h1>Hello, Angular (Standalone)!</h1>`
})
export class HelloWorldComponent {}

Notice that we no longer need to declare this component inside AppModule. It is self-contained.


5.3 Bootstrapping a Standalone Application

Instead of bootstrapping with AppModule, we use bootstrapApplication() directly in main.ts:

import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent)
  .catch(err => console.error(err));

This makes the entry point simpler and removes the need for a root module altogether.


5.4 Importing Dependencies

With NgModules, you imported dependencies inside the imports array of the module.

With standalone components, you import them directly in the component metadata:

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-user-form',
  standalone: true,
  imports: [CommonModule, FormsModule],
  template: `
    <input [(ngModel)]="username" placeholder="Enter your name">
    <p>You typed: {{ username }}</p>
  `
})
export class UserFormComponent {
  username = '';
}

The imports array now lives inside the component, not a module.


5.5 Feature Composition with Standalone APIs

Standalone doesn’t mean isolated — you can still build large applications by composing features.

For example, a feature component can import another standalone component directly:

@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [HelloWorldComponent],
  template: `
    <h2>Dashboard</h2>
    <app-hello-world></app-hello-world>
  `
})
export class DashboardComponent {}

This eliminates the need to juggle multiple feature.modules.ts files.


5.6 Migration Path: From NgModules to Standalone

The Angular team designed standalone to be incremental. You don’t have to rewrite your app all at once.

Migration strategies:

  1. Start with new components as standalone.
  2. Gradually migrate old components/modules when convenient.
  3. Eventually, replace AppModule with bootstrapApplication().

Angular provides utilities like ng generate component --standalone to make this easier.


5.7 Pros and Cons

Advantages of Standalone

  • Simpler mental model (no more "where do I declare this?").
  • Less boilerplate — no NgModule files.
  • Encourages component-driven architecture.
  • Direct, explicit imports (closer to how ES modules work).

Disadvantages

  • Some enterprise teams already invested heavily in NgModules.
  • Learning curve for developers familiar with the “old way.”
  • Some third-party libraries still assume NgModules (though this is changing).

5.8 Best Practices

  • For new projects, prefer standalone.
  • For existing projects, mix-and-match — no need to refactor everything at once.
  • Keep imports clean: only import what the component actually needs.
  • Use feature folders (per-component or per-feature) to maintain structure without modules.

5.9 Summary

Standalone components represent a shift toward simplicity in Angular development. While NgModules are still supported, standalone APIs streamline the developer experience and align Angular with modern JavaScript practices.

In the next chapter, we’ll explore Signals vs. Virtual DOM, a major innovation that shows how Angular’s change detection model differs from frameworks like React — and why Angular signals are a game-changer.

Chapter 6: Signals, Zone.js vs vDom

Angular has always stood apart from frameworks like React and Vue in how it handles change detection. While React relies on the Virtual DOM (VDOM) to determine what needs updating, Angular historically used a zone-based change detection system.

With Angular v16, the framework introduced signals: a reactive primitive that lets you track and respond to state changes directly. Signals give Angular developers a more predictable, fine-grained, and efficient way of managing reactivity.


6.1 How React and Vue Handle Changes: The Virtual DOM

In a Virtual DOM approach (React, Vue), the UI is represented by a lightweight in-memory copy of the DOM. When state changes:

  1. The entire component tree (or subtree) is re-rendered.
  2. The new virtual DOM is compared (diffed) against the old one.
  3. The framework applies only the differences to the real DOM.

This works well but introduces overhead:

  • Constant re-renders.
  • Complex diffing algorithms.
  • Unclear data flow for beginners.

6.2 How Angular Traditionally Worked: Zone.js

Before signals, Angular relied on zone-based change detection. This means Angular automatically keeps track of all asynchronous events in your app (clicks, HTTP responses, timers, promises, etc.) and re-runs change detection after each event.

Angular does this with the help of Zone.js, a library that monkey-patches browser APIs like setTimeout, addEventListener, and XMLHttpRequest.

6.2.1 How it Works (Step by Step)

  1. A user interacts with your app (e.g., clicks a button).
  2. Zone.js intercepts the event.
  3. After the event handler finishes, Angular’s change detection runs through the entire component tree.
  4. Angular compares bindings in templates with their current values.
  5. If something changed, Angular updates the DOM.

6.2.2 Example: Counter with Zones

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

@Component({
  selector: 'app-zone-counter',
  template: `
    <h2>Count: {{ count }}</h2>
    <button (click)="increment()">Increment</button>
    <button (click)="incrementLater()">Increment in 1s</button>
  `
})
export class ZoneCounterComponent {
  count = 0;

  increment() {
    this.count++;
  }

  incrementLater() {
    setTimeout(() => {
      this.count++;
      // No need to tell Angular — Zone.js triggers change detection
    }, 1000);
  }
}

What happens:

  • When you click Increment, Angular detects the click event, updates count, and re-renders the view.
  • When you click Increment in 1s, setTimeout is patched by Zone.js. When the callback runs, Zone.js tells Angular: “Hey, something just happened!” Angular then runs change detection, notices count changed, and updates the DOM.

Without Zone.js, Angular wouldn’t know the async callback finished, and the UI would stay stale.


6.2.3 The Downside

Zone-based detection is convenient, but it can be expensive:

  • Angular re-runs change detection across the entire component tree — even if only one component changed.
  • In large apps, this can lead to performance bottlenecks.
  • Developers had to manually optimize with:
    • ChangeDetectionStrategy.OnPush
    • async pipes
    • Immutability patterns

6.3 Enter Angular Signals

Signals are Angular’s answer to fine-grained reactivity.
A signal is a special object that holds a value. When the value changes, Angular knows exactly which components or computed values depend on it and updates only those.

Think of signals as reactive variables with built-in subscriptions.

This eliminates unnecessary re-checking of unrelated components and gives Angular fine-grained precision, similar to Solid.js or Svelte.

Creating a Signal

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

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <h2>Count: {{ count() }}</h2>
    <button (click)="increment()">Increment</button>
  `
})
export class CounterComponent {
  // A signal that holds a number
  count = signal(0);

  increment() {
    this.count.update(c => c + 1);
  }
}

Notice:

  • You call the signal like a function (count()) to read its value.
  • You update the signal with .set(newValue) or .update(callback).

6.4 Computed Signals

You can derive values from signals with computed().

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

@Component({
  selector: 'app-cart',
  standalone: true,
  template: `
    <p>Items: {{ items() }}</p>
    <p>Total: {{ total() }}</p>
  `
})
export class CartComponent {
  items = signal([10, 20, 30]);

  // Automatically recalculates when items change
  total = computed(() => this.items().reduce((a, b) => a + b, 0));
}

Here, total automatically updates when items changes — no manual subscriptions needed.


6.5 Effects

Sometimes you want to react to changes with side effects (logging, API calls, DOM interactions). Use effect().

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

@Component({
  selector: 'app-logger',
  standalone: true,
  template: `
    <input [(ngModel)]="username()" (ngModelChange)="username.set($event)">
  `
})
export class LoggerComponent {
  username = signal('');

  constructor() {
    effect(() => {
      console.log('Username changed:', this.username());
    });
  }
}

Every time username changes, the effect runs automatically.


Signals vs Angular Zone.js Change Detection

Angular traditionally uses Zone.js for change detection, which patches async APIs (setTimeout, Promise, DOM events) and triggers a global check of the component tree. With Signals, Angular can skip most of this overhead by directly tracking dependencies between state and templates.


Comparison Table

FeatureZone.js Change Detection (Traditional Angular)Signals (Angular 16+)
Trigger mechanismZone.js patches async events (setTimeout, HTTP, user events) → runs global change detectionDirectly marks affected signals as dirty
Update granularityWalks the entire component tree (unless OnPush optimizations are used)Updates only the exact template bindings that depend on changed signals
PerformanceCan be wasteful on large trees (many unaffected components still checked)Highly efficient: no tree traversal, no redundant checks
Developer mental model“Angular checks everything after any async event”“Only dependent values re-render”
Optimizations neededOnPush strategy, immutable patterns, ChangeDetectorRef tweaksRarely needed — fine-grained reactivity is built in
DebuggingMust understand zones, CD cycles, and OnPush behaviorIntuitive: signals update what they’re connected to
Backward compatibilityDefault for all Angular apps historicallyCan coexist with zones, migration is incremental

6.7 When to Use Signals

  • Managing local component state (like counters, form values).
  • Deriving computed values (like totals, filters, derived flags).
  • Replacing BehaviorSubject for simpler reactive patterns.
  • Gradually introducing into existing apps (they coexist with Observables).

6.8 Best Practices

  • Use signals for state you want to reactively update in the template.
  • Use computed instead of recalculating in the template.
  • Use effect only for side effects (logging, DOM manipulation, API calls).
  • Don’t overuse signals — large-scale app state may still benefit from NgRx or global stores.

Chapter 7: Services and Dependency Injection

So far, our components have managed their own data and logic. But what if we need to share functionality across components? For example:

  • A logging service used by multiple features.
  • A data service to fetch users from an API.
  • A state service to hold global settings.

Putting all this logic inside components would make them messy and hard to reuse. Angular’s solution is services, combined with a powerful dependency injection system.


7.1 What Are Services?

A service is a class that encapsulates logic, data, or functionality that you want to share across components. Services are not tied to the DOM; instead, they handle business logic.

Example: a simple logging service.

export class LoggerService {
  log(message: string) {
    console.log('[LOG]:', message);
  }
}

This service can now be reused anywhere in your application.


7.2 The @Injectable Decorator

To make a service available for Angular’s DI system, you mark it with @Injectable().

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

@Injectable({
  providedIn: 'root' // service is available app-wide
})
export class LoggerService {
  log(message: string) {
    console.log('[LOG]:', message);
  }
}

Here:

  • @Injectable() tells Angular this class can participate in DI.
  • providedIn: 'root' makes the service a singleton (one instance shared across the whole app).

7.3 Using a Service in a Component

The most common way is constructor injection.

import { Component } from '@angular/core';
import { LoggerService } from './logger.service';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  template: `<button (click)="logMessage()">Log</button>`
})
export class DashboardComponent {
  constructor(private logger: LoggerService) {}

  logMessage() {
    this.logger.log('Dashboard button clicked!');
  }
}

Angular sees the LoggerService type in the constructor, finds it in the injector tree, and provides an instance.


7.4 Hierarchical Injectors

Angular uses a hierarchical injector system:

  • Root injector – created when the app starts (services with providedIn: 'root').
  • Component injectors – each component can have its own injector.

This means you can scope services differently.

Example: Service Scoped to a Component

@Component({
  selector: 'app-user-list',
  standalone: true,
  providers: [LoggerService], // new instance for this component
  template: `
    <button (click)="logMessage()">Log</button>
  `
})
export class UserListComponent {
  constructor(private logger: LoggerService) {}

  logMessage() {
    this.logger.log('UserList button clicked!');
  }
}

Here, every UserListComponent gets its own instance of LoggerService.


7.5 Singleton Services and Scopes

  • Singleton: If you provide a service in the root injector, the same instance is reused everywhere.
  • Scoped: If you provide a service at a component level, Angular creates a new instance for that component (and its children).
  • Feature-scoped: If you provide a service in a lazy-loaded module, Angular creates a new instance for that module.

This flexibility allows you to balance shared state and isolation.


7.6 Constructor Injection vs. @inject

Traditionally, Angular used constructor injection:

constructor(private logger: LoggerService) {}

But starting with Angular v14+, you can also use the new inject() function inside your class:

import { Component, inject } from '@angular/core';
import { LoggerService } from './logger.service';

@Component({
  selector: 'app-settings',
  standalone: true,
  template: `<button (click)="logMessage()">Log</button>`
})
export class SettingsComponent {
  private logger = inject(LoggerService);

  logMessage() {
    this.logger.log('Settings updated!');
  }
}

When to Use Each

  • Constructor injection: still the default, especially when injecting multiple services (cleaner and explicit).
  • inject() function: useful in
    • standalone components (avoids clutter in constructor),
    • utility classes (where no constructor exists),
    • signals/effects or static methods.

Example: Using inject() in a signal-based service:

import { Injectable, signal, inject } from '@angular/core';
import { LoggerService } from './logger.service';

@Injectable({ providedIn: 'root' })
export class CounterService {
  private logger = inject(LoggerService);
  count = signal(0);

  increment() {
    this.count.update(c => c + 1);
    this.logger.log(`Count is now ${this.count()}`);
  }
}

7.7 Best Practices

  • Prefer providedIn: 'root' for global services.
  • Use component-level providers for state that should reset with the component.
  • Don’t overload services with UI logic — keep them focused on business logic.
  • Use constructor injection for clarity, but inject() for advanced cases.
  • Avoid creating circular dependencies (service A depends on service B and vice versa).

7.8 Summary

  • Services encapsulate shared logic and data.
  • @Injectable makes them available to Angular’s DI system.
  • Angular injectors are hierarchical, supporting global, feature, and component-level scoping.
  • Services can be singleton or scoped depending on where they are provided.
  • Angular now supports both constructor injection and the new inject() function, giving you more flexibility.

Chapter 8: Routing and Navigation

Most useful applications aren’t made up of a single screen. They involve multiple views: a dashboard, a list of users, a detail page, a settings panel. To move between these, we need a routing system.

Angular provides a powerful, built-in Router that allows you to:

  • Define application routes.
  • Navigate between views.
  • Pass parameters.
  • Protect certain routes with guards.
  • Optimize loading with lazy modules or standalone APIs.

In this chapter, we’ll build a strong understanding of Angular’s routing system, from the basics to advanced features.


8.1 Setting Up Routing

When generating a new Angular project, the CLI asks:

? Would you like to add Angular routing? (y/N)

If you answer Yes, it configures RouterModule automatically. If not, you can add it later:

import { provideRouter, Routes } from '@angular/router';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';

const routes: Routes = [];

bootstrapApplication(AppComponent, {
  providers: [provideRouter(routes)]
});

Here we’re using the standalone API (provideRouter), which is the modern way to set up routing.


8.2 Defining Routes

A route maps a URL path to a component.

import { Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';

export const routes: Routes = [
  { path: '', component: HomeComponent }, // default route
  { path: 'about', component: AboutComponent }
];

In your root template (app.component.html), add the <router-outlet> directive:

<nav>
  <a routerLink="">Home</a>
  <a routerLink="about">About</a>
</nav>

<router-outlet></router-outlet>

Now your app has two navigable views.


8.3 Navigating Between Routes

There are three ways to navigate:

  1. Declarative links (preferred for most cases):
<a routerLink="/about">About</a>
  1. Programmatic navigation using Router:
import { Router } from '@angular/router';

@Component({ /* ... */ })
export class HomeComponent {
  constructor(private router: Router) {}

  goToAbout() {
    this.router.navigate(['/about']);
  }
}
  1. Relative navigation (relative to current route):
this.router.navigate(['../profile'], { relativeTo: this.route });

8.4 Route Parameters

Sometimes we need dynamic routes, like /users/42.

{ path: 'users/:id', component: UserDetailComponent }

In UserDetailComponent, you can access the parameter:

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-user-detail',
  standalone: true,
  template: `<h2>User ID: {{ userId }}</h2>`
})
export class UserDetailComponent {
  userId!: string;

  constructor(private route: ActivatedRoute) {
    this.userId = this.route.snapshot.paramMap.get('id')!;
  }
}

For reactive updates (when navigating between users while staying on the same page):

this.route.paramMap.subscribe(params => {
  this.userId = params.get('id')!;
});

8.5 Query Parameters and Fragments

Add optional data to a URL with query parameters:

<a [routerLink]="['/search']" [queryParams]="{ q: 'angular' }">
  Search Angular
</a>

In the component:

this.route.queryParamMap.subscribe(params => {
  console.log(params.get('q')); // "angular"
});

Fragments (#anchor) are also supported via this.route.fragment.


8.6 Nested Routes (Child Routes)

You can define child routes inside a parent.

{ 
  path: 'admin', 
  component: AdminComponent,
  children: [
    { path: 'users', component: AdminUsersComponent },
    { path: 'settings', component: AdminSettingsComponent }
  ]
}

<router-outlet> inside AdminComponent renders the child views.


8.7 Lazy Loading

For large apps, it’s inefficient to load everything up front. Lazy loading lets Angular load features only when needed.

With standalone APIs:

{
  path: 'products',
  loadComponent: () =>
    import('./products/products.component').then(m => m.ProductsComponent)
}

This way, the Products feature is only downloaded when the user navigates to /products.


8.8 Route Guards

Some routes should only be accessible under certain conditions — e.g., if the user is logged in. Guards let you protect routes.

Example AuthGuard:

import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

export const authGuard: CanActivateFn = () => {
  const auth = inject(AuthService);
  const router = inject(Router);

  return auth.isLoggedIn() ? true : router.parseUrl('/login');
};

Use it in routes:

{ path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] }

8.9 Route Resolvers

Resolvers let you fetch data before navigation completes, ensuring the component has the data it needs immediately.

import { ResolveFn } from '@angular/router';
import { UserService } from './user.service';

export const userResolver: ResolveFn<any> = (route) => {
  const service = inject(UserService);
  return service.getUser(route.paramMap.get('id')!);
};

Route definition:

{ path: 'users/:id', component: UserDetailComponent, resolve: { user: userResolver } }

Inside the component:

this.route.data.subscribe(data => {
  this.user = data['user'];
});

8.10 Best Practices

  • Keep routes organized by feature.
  • Prefer standalone route definitions (provideRouter).
  • Use lazy loading for large features.
  • Use guards for security, not just UI (still validate on the backend).
  • Avoid deeply nested routes unless truly necessary.

8.11 Summary

  • Angular’s router lets you define paths, navigate, and manage dynamic data in URLs.
  • Routing works with standalone APIs, eliminating the need for NgModules.
  • Features include parameters, query params, nested routes, lazy loading, guards, and resolvers.

Chapter 9: Forms in Angular

Most applications need to collect user input — login pages, search boxes, registration forms, checkout screens. Angular offers two powerful ways to handle forms:

  1. Template-driven forms — simple, declarative, good for small forms.
  2. Reactive forms — explicit, programmatic, great for complex or dynamic forms.

Both approaches share the same goals:

  • Capture user input.
  • Track state (dirty, touched, valid).
  • Validate input.
  • React to changes.

In this chapter, we’ll explore both strategies in detail.


9.1 Template-Driven Forms

Template-driven forms use Angular’s directives like ngModel inside templates. Most of the logic lives in the HTML, and Angular builds the form model under the hood.

Example: Login Form (Template-Driven)

TypeScript (Component):

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

@Component({
  selector: 'app-login',
  standalone: true,
  templateUrl: './login.component.html'
})
export class LoginComponent {
  email: string = '';
  password: string = '';

  login() {
    console.log('Email:', this.email);
    console.log('Password:', this.password);
  }
}

Template:

<form (ngSubmit)="login()">
  <label>Email</label>
  <input type="email" [(ngModel)]="email" name="email" required>

  <label>Password</label>
  <input type="password" [(ngModel)]="password" name="password" required>

  <button type="submit">Login</button>
</form>

✅ Here:

  • [(ngModel)] binds input values to component properties.
  • name attribute is required for Angular to track each control.
  • Validation (required) is handled via HTML5 + Angular’s directives.

9.2 Validation in Template-Driven Forms

Angular automatically applies CSS classes to inputs:

  • ng-valid / ng-invalid
  • ng-touched / ng-untouched
  • ng-dirty / ng-pristine

Example:

<input type="email" [(ngModel)]="email" name="email" required>
<div *ngIf="emailControl.invalid && emailControl.touched">
  Email is required!
</div>

You can also use Angular’s #ref="ngModel" to access state:

<input type="email" [(ngModel)]="email" name="email" required #emailCtrl="ngModel">
<div *ngIf="emailCtrl.invalid && emailCtrl.touched">
  Invalid email!
</div>

9.3 Reactive Forms

Reactive forms are more programmatic. Instead of Angular inferring the form model, you explicitly create it using FormGroup, FormControl, and FormBuilder.

Example: Registration Form (Reactive)

TypeScript (Component):

import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-register',
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './register.component.html'
})
export class RegisterComponent {
  registerForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.registerForm = this.fb.group({
      name: ['', Validators.required],
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(6)]]
    });
  }

  submit() {
    if (this.registerForm.valid) {
      console.log(this.registerForm.value);
    }
  }
}

Template:

<form [formGroup]="registerForm" (ngSubmit)="submit()">
  <label>Name</label>
  <input formControlName="name">
  <div *ngIf="registerForm.get('name')?.invalid && registerForm.get('name')?.touched">
    Name is required
  </div>

  <label>Email</label>
  <input formControlName="email">
  <div *ngIf="registerForm.get('email')?.invalid && registerForm.get('email')?.touched">
    Enter a valid email
  </div>

  <label>Password</label>
  <input type="password" formControlName="password">
  <div *ngIf="registerForm.get('password')?.invalid && registerForm.get('password')?.touched">
    Password must be at least 6 characters
  </div>

  <button type="submit" [disabled]="registerForm.invalid">Register</button>
</form>

9.4 Reactive vs Template-Driven

FeatureTemplate-DrivenReactive
Form model locationIn the template (ngModel)In the component class
Code styleDeclarative, less boilerplateProgrammatic, more explicit
ValidationBuilt-in directivesValidators in code
Best forSimple forms, quick prototypesComplex forms, dynamic logic

9.5 Advanced Reactive Features

9.5.1 FormArray

Dynamic arrays of controls. Example: adding multiple phone numbers.

phones: FormArray;

this.phones = this.fb.array([this.fb.control('')]);

9.5.2 Dynamic Form Controls

Create controls at runtime based on server config.

9.5.3 Async Validators

Check availability with backend (e.g., unique username).

Validators.composeAsync([this.userService.usernameTakenValidator()])

9.8 Forms with Signals

Signals make it easier to manage form state without constantly reaching into FormControl objects. Starting with Angular v17, you can bridge Reactive Forms and Signals using the new valueChanges signal utilities. This approach combines the predictability of signals with the structure of reactive forms.


9.8.1 Converting Form Controls to Signals

Angular provides a helper:

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

This converts an Observable (like FormControl.valueChanges) into a signal.

Example:

import { Component, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { toSignal } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-signal-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <input [formControl]="nameControl">
    <p>You typed: {{ name() }}</p>
  `
})
export class SignalFormComponent {
  nameControl = new FormControl('');
  name = toSignal(this.nameControl.valueChanges, { initialValue: '' });
}

Here:

  • toSignal() wraps the valueChanges Observable.
  • The template can read the value simply by calling name().

9.8.2 Direct Signal State Management

For very simple forms, you don’t even need FormControl. You can bind inputs directly to signals.

Example:

import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-signal-login',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form (ngSubmit)="submit()">
      <label>Email</label>
      <input [(ngModel)]="email()" (ngModelChange)="email.set($event)" name="email">

      <label>Password</label>
      <input type="password" [(ngModel)]="password()" (ngModelChange)="password.set($event)" name="password">

      <button type="submit">Login</button>
    </form>
    <p>Email preview: {{ email() }}</p>
  `
})
export class SignalLoginComponent {
  email = signal('');
  password = signal('');

  submit() {
    console.log('Email:', this.email());
    console.log('Password:', this.password());
  }
}

Here, signal is used directly as the form state — no FormControl or FormGroup needed. This works great for small forms.


9.8.3 Mixing Signals and Reactive Forms

You can combine both approaches: use Reactive Forms for validation and structure, but expose state as signals for easy reactivity.

@Component({
  selector: 'app-register-signal',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="submit()">
      <input formControlName="username" placeholder="Username">
      <p *ngIf="usernameError()">❌ Username is required</p>

      <button type="submit">Register</button>
    </form>
  `
})
export class RegisterSignalComponent {
  form = this.fb.group({
    username: ['', Validators.required]
  });

  username = toSignal(this.form.get('username')!.valueChanges, { initialValue: '' });
  usernameError = computed(() => this.form.get('username')!.invalid && this.form.get('username')!.touched);

  constructor(private fb: FormBuilder) {}

  submit() {
    console.log('Form value:', this.form.value);
  }
}

✅ Here we use:

  • toSignal() to track the username’s value as a signal.
  • computed() to derive validation state reactively.

9.8.4 When to Use Signal-Based Forms

  • ✅ Use direct signals for very simple forms (like a login or search input).
  • ✅ Use Reactive Forms + toSignal() for complex forms where validation, dynamic controls, or async rules are required.
  • 🚫 Avoid replacing all forms with signals today — Angular’s form APIs are still primarily built around FormControl and FormGroup, and signals complement them rather than replace them.

9.9 Chapter Summary

  • Angular supports three styles of forms:
    1. Template-driven (simple, declarative).
    2. Reactive (structured, programmatic).
    3. Signal-based (lightweight, reactive, new in Angular 17).
  • Signals integrate seamlessly with form controls using toSignal().
  • For small forms, signals can completely replace controls.
  • For complex forms, signals work best on top of Reactive Forms.

Chapter 10: HTTP and APIs

Almost every real-world application needs to talk to a backend — to fetch data, submit forms, authenticate users, or save changes. Angular provides a powerful and flexible way to handle HTTP communication via the HttpClient service.

In this chapter, we’ll cover:

  • Setting up HttpClientModule.
  • Making GET, POST, PUT, DELETE requests.
  • Handling errors.
  • Adding headers and query parameters.
  • Using interceptors for authentication and logging.
  • Combining HTTP with signals for reactive state.

10.1 Setting Up HttpClient

To start using HTTP in Angular, import the module:

import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, {
  providers: [provideHttpClient()]
});

👉 With standalone APIs, you use provideHttpClient() instead of importing HttpClientModule.


10.2 Making Requests

All requests are made through the HttpClient service, which Angular injects.

Example: Fetching Users (GET Request)

Service:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class UserService {
  private apiUrl = 'https://jsonplaceholder.typicode.com/users';

  constructor(private http: HttpClient) {}

  getUsers(): Observable<any[]> {
    return this.http.get<any[]>(this.apiUrl);
  }
}

Component:

import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-list',
  standalone: true,
  template: `
    <ul>
      <li *ngFor="let user of users">{{ user.name }}</li>
    </ul>
  `
})
export class UserListComponent implements OnInit {
  users: any[] = [];

  constructor(private userService: UserService) {}

  ngOnInit() {
    this.userService.getUsers().subscribe(data => (this.users = data));
  }
}

10.3 Sending Data (POST, PUT, DELETE)

POST Example (Creating a User)

createUser(user: any): Observable<any> {
  return this.http.post(this.apiUrl, user);
}

PUT Example (Updating a User)

updateUser(id: number, user: any): Observable<any> {
  return this.http.put(`${this.apiUrl}/${id}`, user);
}

DELETE Example (Removing a User)

deleteUser(id: number): Observable<any> {
  return this.http.delete(`${this.apiUrl}/${id}`);
}

10.4 Adding Headers and Query Params

Headers Example:

this.http.get(url, {
  headers: { Authorization: 'Bearer my-token' }
});

Query Params Example:

this.http.get(url, {
  params: { page: '1', limit: '10' }
});

10.5 Error Handling

Always handle errors gracefully using RxJS catchError.

import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';

getUsers(): Observable<any[]> {
  return this.http.get<any[]>(this.apiUrl).pipe(
    catchError(err => {
      console.error('Error fetching users', err);
      return throwError(() => new Error('Failed to fetch users'));
    })
  );
}

10.6 Http Interceptors

Interceptors let you modify requests/responses globally — great for authentication, logging, or error handling.

Example: Auth Interceptor

import { Injectable } from '@angular/core';
import { HttpInterceptorFn } from '@angular/common/http';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authReq = req.clone({
    setHeaders: { Authorization: `Bearer fake-jwt-token` }
  });
  return next(authReq);
};

Register it in your main.ts:

import { withInterceptors } from '@angular/common/http';

bootstrapApplication(AppComponent, {
  providers: [provideHttpClient(withInterceptors([authInterceptor]))]
});

10.7 HTTP + Signals

With Angular 16+, you can combine HttpClient with signals for simpler state management.

import { Injectable, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class PostService {
  posts = signal<any[]>([]);
  loading = signal(false);

  constructor(private http: HttpClient) {}

  loadPosts() {
    this.loading.set(true);
    this.http.get<any[]>('https://jsonplaceholder.typicode.com/posts')
      .subscribe(data => {
        this.posts.set(data);
        this.loading.set(false);
      });
  }
}

Component:

@Component({
  selector: 'app-posts',
  standalone: true,
  template: `
    <button (click)="service.loadPosts()">Load Posts</button>
    <p *ngIf="service.loading()">Loading...</p>
    <ul>
      <li *ngFor="let post of service.posts()">{{ post.title }}</li>
    </ul>
  `
})
export class PostsComponent {
  constructor(public service: PostService) {}
}

Now the template reacts automatically to changes in posts() and loading().


10.8 Using Async/Await with HttpClient

By default, Angular’s HttpClient methods (get, post, etc.) return Observables. This gives you powerful operators, streaming, and cancellation — but sometimes you want the simplicity of async/await.

Angular provides utilities in rxjs to convert Observables to Promises:

  • firstValueFrom() – resolves with the first emitted value.
  • lastValueFrom() – resolves with the last emitted value.

Example: Fetching Data with async/await

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';

@Component({
  selector: 'app-async-posts',
  standalone: true,
  template: `
    <button (click)="loadPosts()">Load Posts</button>
    <ul>
      <li *ngFor="let post of posts">{{ post.title }}</li>
    </ul>
  `
})
export class AsyncPostsComponent {
  posts: any[] = [];

  constructor(private http: HttpClient) {}

  async loadPosts() {
    try {
      const response = await firstValueFrom(
        this.http.get<any[]>('https://jsonplaceholder.typicode.com/posts')
      );
      this.posts = response;
    } catch (err) {
      console.error('Error fetching posts', err);
    }
  }
}

✅ Here:

  • We wrapped the http.get Observable in firstValueFrom().
  • Now we can use await to get the data like a Promise.
  • Errors are caught with try/catch.

Example: POST with Async/Await

async createPost(post: any) {
  return await firstValueFrom(
    this.http.post('https://jsonplaceholder.typicode.com/posts', post)
  );
}

When to Use Async/Await vs Observables

  • Use async/await when:

    • You want simple, one-off calls (e.g., fetching data once).
    • You’re writing code that already uses async/await patterns.
  • Use Observables when:

    • You need streams of data (live updates, multiple emissions).
    • You need operators like map, switchMap, debounceTime.
    • You want cancellation (unsubscribe).

Side-by-Side Comparison

AspectObservable (subscribe)Async/Await (firstValueFrom)
StyleReactive, functionalImperative, synchronous-looking
Multiple emissions✅ Supported🚫 Only first/last value
Cancellation✅ Can unsubscribe🚫 No built-in cancellation
Error handlingerror callbacktry/catch
Best forStreams, continuous updatesOne-off requests, simple flows

10.9 Async/Await with Signals (loading + error + data)

This pattern keeps all request state in a small “store-like” service using signals. Components stay tiny and declarative.

Service (signals + async/await):

import { Injectable, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';

export interface Post {
  id?: number;
  title: string;
  body: string;
}

@Injectable({ providedIn: 'root' })
export class PostsStore {
  // reactive state
  readonly posts  = signal<Post[]>([]);
  readonly loading = signal(false);
  readonly error   = signal<string | null>(null);

  // derived state
  readonly count = computed(() => this.posts().length);
  readonly hasError = computed(() => this.error() !== null);

  constructor(private http: HttpClient) {}

  async loadPosts() {
    this.loading.set(true);
    this.error.set(null);
    try {
      const data = await firstValueFrom(
        this.http.get<Post[]>('https://jsonplaceholder.typicode.com/posts')
      );
      this.posts.set(data);
    } catch (e: any) {
      this.error.set(e?.message ?? 'Failed to load posts');
    } finally {
      this.loading.set(false);
    }
  }

  async addPost(post: Post) {
    this.loading.set(true);
    this.error.set(null);
    try {
      const created = await firstValueFrom(
        this.http.post<Post>('https://jsonplaceholder.typicode.com/posts', post)
      );
      // append immutably so OnPush/change detection + signals stay happy
      this.posts.update(list => [created, ...list]);
    } catch (e: any) {
      this.error.set(e?.message ?? 'Failed to create post');
    } finally {
      this.loading.set(false);
    }
  }

  // handy helper to clear an error from the UI
  clearError() {
    this.error.set(null);
  }
}

Component (tiny, purely declarative):

import { Component } from '@angular/core';
import { PostsStore } from './posts.store';

@Component({
  selector: 'app-posts',
  standalone: true,
  template: `
    <button (click)="store.loadPosts()" [disabled]="store.loading()">Reload</button>

    <form (ngSubmit)="create()" style="margin:1rem 0;">
      <input placeholder="Title" [(ngModel)]="title" name="title">
      <input placeholder="Body"  [(ngModel)]="body"  name="body">
      <button type="submit" [disabled]="store.loading()">Add</button>
    </form>

    <p *ngIf="store.loading()">Loading…</p>
    <p *ngIf="store.hasError()">
      ❌ {{ store.error() }}
      <button (click)="store.clearError()">dismiss</button>
    </p>

    <p *ngIf="!store.loading()">Total: {{ store.count() }}</p>

    <ul>
      <li *ngFor="let p of store.posts()">
        <strong>{{ p.title }}</strong> — {{ p.body }}
      </li>
    </ul>
  `
})
export class PostsComponent {
  title = '';
  body  = '';

  constructor(public store: PostsStore) {}

  async create() {
    await this.store.addPost({ title: this.title, body: this.body });
    this.title = '';
    this.body = '';
  }
}

Why this works well

  • Signals make UI reads trivial: store.loading() / store.error() / store.posts().
  • Async/await keeps request code linear and easy to reason about.
  • Computed (count) demonstrates derived state that auto-updates with no extra wiring.
  • Errors & loading are centralized in the service, not sprinkled through components.

Tip: For sequences (load → then add), await naturally serializes operations. For parallel calls, Promise.all() works fine with firstValueFrom(...)-wrapped requests.

10.10 Best Practice:

  • Don’t replace all Observables with async/await. Instead, use both where they fit best. Async/await is great for one-shot requests; Observables shine for reactive, continuous data flows.
  • Centralize API logic in services, not components.
  • Always handle errors (network failures happen).
  • Use interceptors for authentication, logging, and retry strategies.
  • Combine signals with HTTP for reactive UI updates.
  • Keep URLs/configs in environment files for easy switching (dev, prod).

10.11 Chapter Summary

  • Angular’s HttpClient returns Observables by default.
  • You can use async/await with firstValueFrom() or lastValueFrom() to simplify code.
  • Observables remain more powerful for streams, operators, and cancellation.

Chapter 11: State Management in Angular

Every application needs to manage state — the data that drives what users see on the screen.
Examples of state:

  • A shopping cart’s contents.
  • A logged-in user’s profile.
  • A list of posts fetched from an API.

As apps grow, managing state becomes more complex. Angular provides several options, ranging from simple local state in components to enterprise-scale solutions like NgRx.


11.1 Local State

The simplest way to manage state is inside a component.

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Count: {{ count }}</p>
    <button (click)="increment()">+</button>
  `
})
export class CounterComponent {
  count = 0;

  increment() {
    this.count++;
  }
}

✅ Works fine for isolated components.
❌ Breaks down when multiple components need the same state.


11.2 Parent → Child

Use @Input() on the child to accept data from the parent.

Child (profile-card.component.ts)

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

@Component({
  selector: 'app-profile-card',
  standalone: true,
  template: `
    <article>
      <h3>{{ name }}</h3>
      <p *ngIf="bio">{{ bio }}</p>
    </article>
  `
})
export class ProfileCardComponent {
  @Input() name = 'Anonymous';
  @Input() bio?: string;
}

Parent (team.component.ts)

import { Component } from '@angular/core';
import { ProfileCardComponent } from './profile-card.component';

@Component({
  selector: 'app-team',
  standalone: true,
  imports: [ProfileCardComponent],
  template: `
    <app-profile-card
      [name]="leadName"
      [bio]="leadBio">
    </app-profile-card>
  `
})
export class TeamComponent {
  leadName = 'Aisha Khan';
  leadBio  = 'Frontend lead focused on design systems.';
}

Key idea: data flows down from parent to child.


11.3 Sibling ↔ Sibling

Components can’t pass inputs directly to siblings. The classic pattern is:

  1. Sibling A emits an event (e.g., a selected value)
  2. Parent handles the event and updates state
  3. Parent passes updated state down to Sibling B via @Input()

Sibling A: selects a color (color-picker.component.ts)

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

@Component({
  selector: 'app-color-picker',
  standalone: true,
  template: `
    <label>Pick a color:</label>
    <button (click)="choose('red')">Red</button>
    <button (click)="choose('green')">Green</button>
    <button (click)="choose('blue')">Blue</button>
  `
})
export class ColorPickerComponent {
  @Output() colorChange = new EventEmitter<string>();
  choose(color: string) { this.colorChange.emit(color); }
}

Sibling B: displays the color (color-preview.component.ts)

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

@Component({
  selector: 'app-color-preview',
  standalone: true,
  template: `
    <div
      style="width:120px;height:60px;border:1px solid #ccc;"
      [style.background]="color">
    </div>
    <p>Selected: {{ color || 'none' }}</p>
  `
})
export class ColorPreviewComponent {
  @Input() color = '';
}

Parent: mediates between siblings (color-playground.component.ts)

import { Component } from '@angular/core';
import { ColorPickerComponent } from './color-picker.component';
import { ColorPreviewComponent } from './color-preview.component';

@Component({
  selector: 'app-color-playground',
  standalone: true,
  imports: [ColorPickerComponent, ColorPreviewComponent],
  template: `
    <app-color-picker (colorChange)="onColor($event)"></app-color-picker>
    <app-color-preview [color]="selectedColor"></app-color-preview>
  `
})
export class ColorPlaygroundComponent {
  selectedColor = '';
  onColor(color: string) { this.selectedColor = color; }
}

Why this matters: This “lift state up” pattern keeps siblings decoupled and predictable.

Tip: For many cross-sibling interactions across distant branches, prefer a shared service with signals or a global store (NgRx) instead of chaining many parent mediators.


11.5 Using @ViewChild()

@ViewChild() lets a parent query a child (component, directive, or native element) that exists in its own template. Common uses:

  • Call a public method on the child component
  • Focus an input (access native element)
  • Read internal child state (read-only preferred)

A) Call a child component method

Child (modal.component.ts)

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

@Component({
  selector: 'app-modal',
  standalone: true,
  template: `
    <dialog #dlg>
      <h3>Modal</h3>
      <p>Hi there!</p>
      <button (click)="close()">Close</button>
    </dialog>
  `
})
export class ModalComponent {
  private dialogEl!: HTMLDialogElement;

  // Angular calls after view init; link the native element
  ngAfterViewInit() {
    this.dialogEl = (document.querySelector('app-modal dialog') as HTMLDialogElement);
  }

  open()  { this.dialogEl?.showModal(); }
  close() { this.dialogEl?.close(); }
}

Parent uses @ViewChild to drive it (page.component.ts)

import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { ModalComponent } from './modal.component';

@Component({
  selector: 'app-page',
  standalone: true,
  imports: [ModalComponent],
  template: `
    <button (click)="open()">Open Modal</button>
    <app-modal></app-modal>
  `
})
export class PageComponent implements AfterViewInit {
  @ViewChild(ModalComponent) modal?: ModalComponent;

  ngAfterViewInit() {
    // modal is available here
  }

  open() {
    this.modal?.open();
  }
}

Note: In real apps, it’s cleaner to reference the dialog with a @ViewChild('dlg') template ref rather than querySelector; shown here for clarity. See the native element example below.

B) Focus an input via native element

Template reference + ElementRef

import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-search',
  standalone: true,
  template: `
    <input #term type="text" placeholder="Search…" />
    <button (click)="focus()">Focus</button>
  `
})
export class SearchComponent implements AfterViewInit {
  @ViewChild('term') termInput?: ElementRef<HTMLInputElement>;

  ngAfterViewInit() {
    // Optionally autofocus when ready:
    this.termInput?.nativeElement.focus();
  }

  focus() {
    this.termInput?.nativeElement.focus();
  }
}

Key points

  • @ViewChild() resolves after the view is initialized → use ngAfterViewInit.
  • Prefer calling public methods on a child rather than poking internal fields.
  • For DOM, access via ElementRef and avoid direct DOM manipulation when possible (use Angular bindings).
  • If the element is created conditionally (*ngIf), set @ViewChild(..., { static: false }) (default) and ensure it exists before using it.

When to use which?

  • @Input() (parent → child): pass data down. Simple and explicit.
  • Sibling ↔ Sibling via Parent: raise an event from one sibling, update state in parent, pass down to the other via @Input().
    • For many siblings/features → consider a shared service with signals or NgRx.
  • @ViewChild() (parent → child instance): call public methods (e.g., open modal, reset form) or interact with native elements (focus). Use sparingly to avoid tight coupling.

11.6 Service-Based State (Shared Services)

The most common Angular pattern is to use services with DI to share state.

@Injectable({ providedIn: 'root' })
export class CartService {
  private items: string[] = [];

  addItem(item: string) { this.items.push(item); }
  getItems() { return this.items; }
}

Injected into components:

@Component({
  selector: 'app-cart',
  standalone: true,
  template: `
    <button (click)="add()">Add Item</button>
    <ul><li *ngFor="let item of items">{{ item }}</li></ul>
  `
})
export class CartComponent {
  items: string[] = [];
  constructor(private cart: CartService) {
    this.items = this.cart.getItems();
  }
  add() { this.cart.addItem('Book'); }
}

✅ Simple, effective, easy to test.
❌ State changes don’t automatically notify components (unless combined with Observables or Signals).


11.7 State with RxJS Observables

Before signals, the main reactive pattern was BehaviorSubject or ReplaySubject.

@Injectable({ providedIn: 'root' })
export class UserService {
  private userSubject = new BehaviorSubject<User | null>(null);
  user$ = this.userSubject.asObservable();

  setUser(user: User) { this.userSubject.next(user); }
}

Component:

@Component({
  selector: 'app-profile',
  standalone: true,
  template: `<p *ngIf="user$ | async as user">Welcome, {{ user.name }}</p>`
})
export class ProfileComponent {
  user$ = this.userService.user$;
  constructor(private userService: UserService) {}
}

✅ Very powerful, integrates with Angular’s async pipe.
❌ More boilerplate and learning curve than signals.


11.8 @Input() with Signals

Normally, @Input() is just a property that Angular assigns when the parent updates it. But with Angular’s signal inputs, you can turn inputs into reactive signals that auto-update when the parent changes.


Example 1: Simple Input Signal

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

@Component({
  selector: 'app-greeting',
  standalone: true,
  template: `<h2>Hello, {{ name() }}!</h2>`
})
export class GreetingComponent {
  // input() makes a signal input
  name = input<string>('Guest');
}

Parent component:

@Component({
  selector: 'app-home',
  standalone: true,
  imports: [GreetingComponent],
  template: `
    <app-greeting [name]="userName"></app-greeting>
    <button (click)="userName = 'Alice'">Set Name</button>
  `
})
export class HomeComponent {
  userName = 'Bob';
}
  • Initially renders: Hello, Bob!
  • After clicking button: Hello, Alice!
  • If no name input is provided, defaults to Guest.

Example 2: Computed Signal from an Input

You can derive values from inputs using computed().

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

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <h3>{{ displayName() }}</h3>
  `
})
export class UserCardComponent {
  firstName = input<string>();
  lastName = input<string>();

  displayName = computed(() =>
    `${this.firstName() ?? ''} ${this.lastName() ?? ''}`.trim()
  );
}

Parent component:

<app-user-card [firstName]="'Jane'" [lastName]="'Doe'"></app-user-card>

Renders: Jane Doe.


Example 3: Input Signals with Defaults

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Start: {{ start() }}</p>
    <p>Count: {{ count }}</p>
    <button (click)="increment()">+</button>
  `
})
export class CounterComponent {
  start = input<number>(0); // default value
  count = 0;

  increment() {
    this.count++;
  }
}

If parent provides [start]="10", it shows Start: 10. If not, defaults to 0.


Key Benefits of Input Signals

  • ✅ Reactive by default — no need for ngOnChanges().
  • ✅ Can be combined with computed signals.
  • ✅ Can provide default values.
  • ✅ Cleaner and more ergonomic than traditional @Input().

11.9 NgRx: Enterprise-Scale State Management

For large applications, Angular offers NgRx, inspired by Redux. NgRx uses:

  • Store – a single global state tree.
  • Actions – events that describe state changes.
  • Reducers – pure functions that update state.
  • Effects – handle side effects (API calls).

Example: Counter with NgRx

Action:

export const increment = createAction('[Counter] Increment');

Reducer:

export const counterReducer = createReducer(0,
  on(increment, state => state + 1)
);

Component:

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Count: {{ count$ | async }}</p>
    <button (click)="increment()">+</button>
  `
})
export class CounterComponent {
  count$ = this.store.select('count');
  constructor(private store: Store<{ count: number }>) {}
  increment() { this.store.dispatch(increment()); }
}

✅ Scales to very large apps.
✅ Predictable and testable.
❌ Steeper learning curve and boilerplate-heavy for small apps.


11.7 Choosing the Right Approach

ApproachBest For
Local Component StateSimple, isolated UI state
@Input/@OutputParent-child communication
Service (Plain)Shared logic/data between few components
Service + SignalsMedium apps needing reactivity
RxJS ObservablesComplex async streams, multi-subscriber
NgRxEnterprise-scale apps with global state

11.8 Best Practices

  • Start simple (component or service).
  • Introduce signals for reactivity instead of manually wiring RxJS everywhere.
  • Use RxJS when you truly need streams or complex async handling.
  • Adopt NgRx only when your app grows beyond what services/signals can handle.

11.9 Summary

  • State management ranges from local variables to enterprise libraries.
  • Signals bring fine-grained reactivity to Angular apps.
  • NgRx is still the go-to for large-scale, complex apps.
  • Always choose the simplest tool that meets your needs.

Chapter 12: Directives in Angular

Directives are Angular’s way of extending HTML with custom behavior. While components control entire views, directives attach behavior to existing DOM elements or templates.

There are three main kinds of directives in Angular:

  1. Component Directives – technically directives with templates.
  2. Structural Directives – change the DOM structure (e.g., *ngIf, *ngFor).
  3. Attribute Directives – change the appearance or behavior of elements (e.g., ngClass, custom highlight directive).

12.1 Built-in Attribute Directives

ngClass

Dynamically apply CSS classes:

<div [ngClass]="{ active: isActive, disabled: isDisabled }">Hello</div>

ngStyle

Apply inline styles dynamically:

<div [ngStyle]="{ color: isActive ? 'green' : 'red' }">Status</div>

12.2 Structural Directives

Structural directives shape or reshape the DOM. They use * prefix (syntactic sugar).

*ngIf

<p *ngIf="isLoggedIn">Welcome back!</p>

*ngFor

<li *ngFor="let item of items; index as i">
  {{ i }} – {{ item }}
</li>

12.3 Creating a Custom Attribute Directive

Let’s create a highlight directive.

import { Directive, ElementRef, HostListener } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  constructor(private el: ElementRef) {}

  @HostListener('mouseenter') onMouseEnter() {
    this.el.nativeElement.style.backgroundColor = 'yellow';
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.el.nativeElement.style.backgroundColor = '';
  }
}

Usage:

<p appHighlight>Hover over me!</p>

12.4 The New Angular Block Syntax (@for, @if)

Starting with Angular v17, we can use new block directives with @ syntax, replacing *ngFor and *ngIf.

Example: @if

@if (isLoggedIn) {
  <p>Welcome back!</p>
} @else {
  <p>Please log in</p>
}

✅ Clearer than *ngIf with else templates.


Example: @for

<ul>
  @for (item of items; track item.id; let i = $index) {
    <li>{{ i }} – {{ item.name }}</li>
  }
</ul>

✅ Differences from *ngFor:

  • No * prefix — uses block { ... } syntax.
  • Built-in tracking with track.
  • Cleaner handling of loop variables ($index, $first, $last, $odd, $even).
  • Easier to read in nested structures.

12.5 Comparison: *ngFor vs @for

Feature*ngFor@for (Angular v17+)
Syntax*ngFor="let item of items"@for (item of items) { ... }
Loop variablesindex as i, first, last, etc.$index, $first, $last, $odd, $even
TrackingtrackBy: fntrack item.id (inline)
ReadabilityNested <ng-template> for else casesBlock structure, cleaner
PerformanceGoodBetter, with compiler optimizations
RecommendationLegacy support, still worksPrefer @for for new projects

12.6 Migration Notes

  • *ngFor and *ngIf still work; plan for removal in version 22
  • New block directives are faster and more readable.
  • @for and @if is the recommended style.

12.7 Summary

  • Directives extend HTML behavior in Angular.
  • Attribute directives (e.g., ngClass) modify styles or behavior.
  • Structural directives (*ngIf, *ngFor) change DOM structure.
  • You can write your own custom directives for reusable logic.
  • Angular v17 introduces block syntax (@if, @for) — a modern, cleaner, and more optimized alternative.

Chapter 13: Pipes and Defer in Angular

Pipes are one of Angular’s most powerful template features. They let you transform values for display without changing the underlying data. Meanwhile, Angular v17 introduced @defer blocks, which delay rendering until certain conditions are met. Together, these features improve readability, performance, and user experience.


13.1 What Are Pipes?

A pipe is a function that transforms data in templates.

Example:

<p>{{ today | date }}</p>

Here, the date pipe formats a Date object into a human-readable string.


13.2 Built-in Pipes

Angular ships with many useful pipes:

  • DatePipe: {{ today | date:'short' }}
  • CurrencyPipe: {{ price | currency:'USD' }}
  • DecimalPipe: {{ num | number:'1.2-2' }}
  • PercentPipe: {{ ratio | percent }}
  • AsyncPipe: {{ data$ | async }}
  • SlicePipe: {{ items | slice:0:3 }}

✅ Pipes are pure by default — they only re-run when input values change.


13.3 Custom Pipes

You can create your own pipes for specific transformations.

Example: CapitalizePipe

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'capitalize',
  standalone: true
})
export class CapitalizePipe implements PipeTransform {
  transform(value: string): string {
    return value.charAt(0).toUpperCase() + value.slice(1);
  }
}

Usage:

<p>{{ 'angular' | capitalize }}</p>
<!-- Output: Angular -->

13.4 Pure vs Impure Pipes

  • Pure pipes (default): Only re-run when input changes (efficient).
  • Impure pipes (pure: false): Re-run on every change detection cycle.

Use impure pipes sparingly (e.g., for filtering arrays in real time).


13.5 The Async Pipe

The async pipe is special: it subscribes to Observables or Promises automatically and unsubscribes when the component is destroyed.

Example:

<p *ngIf="user$ | async as user">Hello {{ user.name }}</p>

✅ Avoids manual .subscribe() and memory leaks.


13.6 Pipes vs Signals

With Angular signals, some transformations can move into computed signals.

Example with signals:

name = signal('angular');
capitalized = computed(() => this.name().toUpperCase());

Template:

<p>{{ capitalized() }}</p>

✅ Use pipes for template-level formatting.
✅ Use signals for state transformations in TypeScript.


13.7 The New @defer Block

Angular v17 introduced @defer, a structural block directive that delays rendering of parts of the template until:

  • The component is idle.
  • A specific trigger happens.
  • Or manually specified conditions are met.

This improves performance by not rendering heavy components immediately.

Example: Lazy Component Rendering

@defer {
  <expensive-chart></expensive-chart>
} @placeholder {
  <p>Loading chart...</p>
}

✅ The <expensive-chart> component is only rendered when the app is idle.
✅ @placeholder shows interim UI.


Example: Conditional Defer with Triggers

@defer (on viewport) {
  <app-ads-banner></app-ads-banner>
}

Here, the banner loads only when it enters the viewport (like lazy-loading images).


Example: Defer with Timer

@defer (on timer(5s)) {
  <app-hint-message></app-hint-message>
}

This shows a hint only after 5 seconds.


13.8 Pipes vs @defer

FeaturePipes@defer
PurposeTransform values for displayControl when to render content
ScopeData formatting in templatesUI performance optimization
Example`{{ pricecurrency }}`
ComplementWorks with @defer to format valuesWorks with pipes for final output

13.9 Summary

  • Pipes transform data directly in templates.
  • Angular ships with powerful built-in pipes (date, async, currency, etc.).
  • You can create custom pipes for reusable formatting.
  • Pipes are pure by default but can be impure.
  • Signals can handle some transformations outside templates.
  • The new @defer block delays rendering for better performance.
  • Together, pipes and @defer improve both data presentation and app responsiveness.

Chapter 14: Standalone-First Architecture

Angular’s introduction of standalone components, directives, and pipes (v14) and standalone route APIs (v15+) marked a major shift in how Angular apps can be built. By Angular v17, the recommended best practice for new projects is standalone-first architecture.

In this chapter, we’ll explore how to design, organize, and scale Angular applications in a module-free world (while still interoperating with modules when needed).


14.1 What Does Standalone-First Mean?

Traditionally, every component had to be declared inside an NgModule. With standalone, a component declares itself:

@Component({
  selector: 'app-hello',
  standalone: true,
  template: `<h1>Hello Standalone!</h1>`
})
export class HelloComponent {}

Standalone-first means:

  • No AppModule (bootstrapped with bootstrapApplication).
  • No feature modules — just folders of standalone components and services.
  • Routes and providers defined at the top level.

14.2 Bootstrapping Without AppModule

import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';

import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient()
  ]
});

✅ The app starts from AppComponent.
✅ All providers (router, HTTP, services) are declared in bootstrapApplication.


14.3 Feature Organization Without Modules

Instead of creating UsersModule, you create a users/ folder with:

users/
  users.component.ts
  user-list.component.ts
  user-detail.component.ts
  users.routes.ts
  user.service.ts

Routes file (users.routes.ts):

import { Routes } from '@angular/router';
import { UserListComponent } from './user-list.component';
import { UserDetailComponent } from './user-detail.component';

export const USERS_ROUTES: Routes = [
  { path: '', component: UserListComponent },
  { path: ':id', component: UserDetailComponent }
];

Imported in app.routes.ts:

import { Routes } from '@angular/router';

export const routes: Routes = [
  { path: 'users', loadChildren: () => import('./users/users.routes').then(r => r.USERS_ROUTES) }
];

✅ No feature module required.


14.4 Shared Features Without Modules

For reusable pieces (like buttons, pipes, or directives), create a shared/ folder with standalone exports.

shared/
  components/
    button.component.ts
  pipes/
    truncate.pipe.ts
  directives/
    autofocus.directive.ts

Each is marked standalone: true, so they can be imported directly into any component:

@Component({
  selector: 'app-home',
  standalone: true,
  imports: [ButtonComponent, TruncatePipe],
  template: `
    <app-button>Click</app-button>
    <p>{{ longText | truncate:20 }}</p>
  `
})
export class HomeComponent {}

14.5 Core Services Without a CoreModule

Previously, singletons were placed in CoreModule. With standalone-first, just provide them in root or via bootstrapApplication.

@Injectable({ providedIn: 'root' })
export class AuthService { /* ... */ }

Or explicitly:

bootstrapApplication(AppComponent, {
  providers: [
    AuthService
  ]
});

14.6 Standalone + Signals = Simpler State

In a standalone-first app, signals pair beautifully with services to manage state. Example:

@Injectable({ providedIn: 'root' })
export class CartStore {
  items = signal<string[]>([]);
  add(item: string) { this.items.update(list => [...list, item]); }
}

Directly consumed in standalone components without any NgModule wiring.


14.7 Interoperability with Modules

Standalone-first doesn’t mean modules are gone:

  • You can import an NgModule inside a standalone component (imports: [FormsModule]).
  • You can use standalone components inside old module-based apps.

This ensures backward compatibility and a smooth migration path.


14.8 Benefits of Standalone-First Architecture

  • 🚀 Simpler mental model: no declarations/import confusion.
  • 🧩 Composable: features are just components + routes.
  • 🏗️ Scalable: folder-based organization replaces module overhead.
  • 🔄 Incremental adoption: standalone works alongside NgModules.
  • ✨ Future-proof: aligns with Angular’s roadmap.

14.9 Example Project Structure

src/app/
  app.component.ts
  app.routes.ts
  users/
    user-list.component.ts
    user-detail.component.ts
    users.routes.ts
    user.service.ts
  products/
    product-list.component.ts
    product-detail.component.ts
    products.routes.ts
    product.service.ts
  shared/
    components/
    pipes/
    directives/

✅ Clear, feature-based, no modules needed.


14.10 Summary

  • Standalone-first is Angular’s new recommended architecture.
  • No AppModule — apps start with bootstrapApplication.
  • Features are organized in folders with routes, services, and standalone components.
  • Shared UI elements live in a shared/ folder.
  • Modules are still supported but optional.

Chapter 15: Change Detection Strategies

Change detection is the mechanism that keeps Angular applications up to date. Whenever a user clicks a button, an HTTP response arrives, or a timer fires, Angular must decide:

Which parts of the DOM should be updated?

This chapter explains how Angular’s change detection works, the available strategies (Default and OnPush), how Zones tie into the process, and how Signals introduce a new era of precision updates.


15.1 The Basics of Change Detection

Angular’s change detection system ensures that values in templates always match the component state.

Example:

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `<p>Count: {{ count }}</p>`
})
export class CounterComponent {
  count = 0;
  increment() { this.count++; }
}
  • When increment() is called, Angular re-checks bindings.
  • If count changed, Angular updates the DOM.

15.2 Zone.js Recap

Angular uses Zone.js to know when “something happened.”

  • Zone.js patches async APIs like setTimeout, Promise, addEventListener.
  • After each async event, Angular runs change detection across the whole component tree.

✅ Simple and automatic.
❌ Can be inefficient — even unrelated components get checked.


15.3 Default Strategy

By default, every time change detection runs, Angular:

  • Walks the entire component tree.
  • Re-evaluates every binding.

This ensures correctness, but in large apps it may become costly.


15.4 OnPush Strategy

To optimize, Angular provides ChangeDetectionStrategy.OnPush.

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

@Component({
  selector: 'app-user',
  standalone: true,
  template: `<p>{{ user.name }}</p>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserComponent {
  @Input() user!: { name: string };
}

With OnPush, Angular only checks the component when:

  1. An @Input() reference changes.
  2. An event inside the component fires.
  3. Manually triggered with ChangeDetectorRef.markForCheck().

✅ Fewer checks, better performance.
❌ Requires immutable data patterns (so Angular can detect changes via reference).


15.5 Manual Control with ChangeDetectorRef

Angular exposes APIs for fine-grained control:

constructor(private cd: ChangeDetectorRef) {}

ngOnInit() {
  setTimeout(() => {
    this.cd.detectChanges(); // run change detection manually
  }, 1000);
}

Useful in edge cases (e.g., third-party libs, WebSockets).


15.6 Signals and Change Detection

Signals change the game.

  • Instead of checking the whole tree, Angular tracks exact dependencies.
  • When a signal changes, Angular knows which bindings depend on it and updates only those.

Example:

@Component({
  selector: 'app-cart',
  standalone: true,
  template: `
    <p>Total items: {{ count() }}</p>
    <button (click)="add()">Add</button>
  `
})
export class CartComponent {
  items = signal<string[]>([]);
  count = computed(() => this.items().length);

  add() {
    this.items.update(list => [...list, 'Book']);
  }
}

Here, only {{ count() }} re-renders when items changes — Angular skips everything else.

✅ No need for OnPush.
✅ No need for manual immutability.
✅ Ultra-efficient reactivity.


15.7 Comparing Strategies

StrategyMechanismBest Use Case
DefaultFull tree check after any async eventSmall/medium apps where simplicity wins
OnPushCheck only on input changes or eventsPerformance-sensitive large apps
SignalsFine-grained dependency trackingModern Angular apps (v16+)

15.8 Migration Path

  • For existing apps: keep using OnPush for optimization.
  • For new apps: adopt signals where possible — it simplifies reactivity.
  • Both models coexist: you can mix OnPush with signals today.

15.9 Summary

  • Change detection is how Angular syncs component state with the DOM.
  • Zone.js runs global checks, which can be wasteful.
  • OnPush reduces checks but requires immutability discipline.
  • Signals provide a future-forward way to achieve fine-grained updates automatically.

Chapter 16: RxJS and Reactive Programming

Angular’s HttpClient, forms, router, and event streams all rely on RxJS (Reactive Extensions for JavaScript). Understanding RxJS is essential for mastering Angular.

This chapter will focus on the theory of reactive programming with RxJS, building a solid foundation before diving into practical patterns later.


16.1 What is Reactive Programming?

Reactive programming is about dealing with data streams (values that arrive over time).
Examples of streams:

  • User clicks.
  • HTTP responses.
  • WebSocket messages.
  • Timer events.

Instead of pulling data (asking for it), we react when data arrives.


16.2 Observables

An Observable is a lazy, push-based collection.

  • Lazy: nothing happens until you subscribe.
  • Push-based: values are delivered as they become available.

Think of an Observable as a producer of values:

import { Observable } from 'rxjs';

const obs = new Observable(observer => {
  observer.next(1);
  observer.next(2);
  setTimeout(() => observer.next(3), 1000);
  observer.complete();
});

16.3 Subscriptions

To consume an Observable, you subscribe:

obs.subscribe({
  next: value => console.log(value),
  error: err => console.error(err),
  complete: () => console.log('Done')
});

Output:

1
2
3  (after 1s)
Done

16.4 Hot vs Cold Observables

  • Cold: starts producing values when you subscribe (e.g., http.get()).
  • Hot: produces values regardless of subscription (e.g., fromEvent(document, 'click')).

This distinction is important for performance and side-effects.


16.5 Subjects

A Subject is both an Observable and an Observer. It allows multicasting values to many subscribers.

import { Subject } from 'rxjs';

const subject = new Subject<number>();

subject.subscribe(v => console.log('A:', v));
subject.subscribe(v => console.log('B:', v));

subject.next(1); // A:1 B:1
subject.next(2); // A:2 B:2

Special subjects:

  • BehaviorSubject: keeps the latest value.
  • ReplaySubject: replays a set number of values to new subscribers.
  • AsyncSubject: emits the last value only when complete.

16.6 Operators

Operators are pure functions that transform streams.
They are what make RxJS powerful.

Categories of operators:

  1. Creation operators (make observables):

    • of(1,2,3), from([1,2,3]), interval(1000), fromEvent(...).
  2. Transformation operators:

    • map, mergeMap, switchMap, concatMap.
  3. Filtering operators:

    • filter, take, skip, debounceTime, distinctUntilChanged.
  4. Combination operators:

    • merge, concat, combineLatest, forkJoin, withLatestFrom.
  5. Error handling operators:

    • catchError, retry, retryWhen.
  6. Utility operators:

    • tap, finalize, delay.

Example: map

of(1, 2, 3).pipe(
  map(x => x * 10)
).subscribe(console.log);
// Output: 10, 20, 30

Example: switchMap

fromEvent(document, 'click').pipe(
  switchMap(() => http.get('/data'))
).subscribe(data => console.log(data));
  • Cancels previous request if another click happens.
  • Useful for search/autocomplete.

16.7 Marble Diagrams

RxJS uses marble diagrams to visualize operators.
Example:

  • Source: --1--2--3--|
  • map(x => x*10)
  • Output: --10--20--30--|

These diagrams help understand how operators manipulate time-based streams.


16.8 RxJS in Angular

Angular integrates RxJS deeply:

  • HttpClient: returns Observables.
  • Forms: form.valueChanges is an Observable.
  • Router: ActivatedRoute.params is an Observable.
  • AsyncPipe: subscribes/unsubscribes automatically.

16.9 Signals vs RxJS

Signals (Angular v16+) offer a simpler reactive model for local state.
RxJS is still essential when:

  • You work with async streams (clicks, websockets, HTTP polling).
  • You need complex transformations (debouncing, combining streams).

Rule of thumb:

  • Use signals for state.
  • Use RxJS for streams.
  • Combine both when needed (toSignal() bridge).

16.10 Summary

  • RxJS brings reactive programming to Angular.
  • Observables are push-based collections of values over time.
  • Subscriptions let you consume observables.
  • Subjects allow multicasting and state handling.
  • Operators transform, filter, and combine streams.
  • RxJS complements signals: both are core to modern Angular.

Chapter 17: Angular Universal (Server-Side Rendering)

Angular applications are Single Page Applications (SPAs) by default. This means the browser downloads JavaScript, then renders the UI dynamically. While this works well for interactivity, it can lead to problems:

  • Slower first paint: users see a blank screen until Angular bootstraps.
  • SEO issues: search engine crawlers may struggle to index dynamic content.
  • Poor performance on slow networks or devices.

Angular Universal solves these challenges by rendering the app on the server first, sending HTML to the client, and then letting Angular take over. This process is called Server-Side Rendering (SSR).


17.1 What is Server-Side Rendering?

  • The server generates HTML for the requested route.
  • The browser displays meaningful content immediately.
  • Angular bootstraps on top of the existing HTML (hydration).

✅ Faster perceived performance.
✅ Better SEO (search engines see full HTML).
✅ Useful for social sharing (link previews).


17.2 CSR vs SSR vs SSG

ApproachHow it worksProsCons
CSR (Client-Side Rendering)Rendered fully in browserFast navigation after load, simple hostingSlow first paint, SEO harder
SSR (Server-Side Rendering)HTML rendered on server, then hydratedFast initial load, SEO-friendlyMore complex build/deploy
SSG (Static Site Generation)Pre-render pages at build timeBest performance, no runtime server loadLimited for dynamic content

Angular Universal supports SSR and SSG (pre-rendering).


17.3 When Should You Use Angular Universal?

  • ✅ Content-heavy apps that need good SEO (blogs, e-commerce).
  • ✅ Apps targeting slow networks or devices.
  • ✅ Apps with social sharing requirements.
  • ❌ Purely internal dashboards may not need SSR.

17.4 The Rendering Lifecycle

  1. User requests /about.
  2. Server runs Angular, generates HTML for /about.
  3. Server sends HTML + JavaScript to client.
  4. Browser shows HTML immediately.
  5. Angular hydrates the page, making it interactive.

17.5 Hydration in Angular

Starting from Angular v17, Angular supports hydration by default:

  • Instead of re-rendering the app after download, Angular attaches event listeners to the server-rendered HTML.
  • This means no “flicker” or DOM duplication.

17.6 Benefits and Trade-Offs

Benefits:

  • Better SEO.
  • Faster Largest Contentful Paint (LCP).
  • Improved accessibility for crawlers.

Trade-offs:

  • Build and deployment complexity.
  • Need a Node.js server (unless using static pre-render).
  • Some client-only APIs (window, document) need special handling.

17.7 Practical Setup: Enabling Angular Universal

Now let’s add SSR step by step.

Step 1: Add Angular Universal

ng add @nguniversal/express-engine

This generates files for SSR support, including:

  • A server.ts file (Express server).
  • Updates to angular.json.
  • A main.server.ts entry point.

Step 2: Run the App with SSR

npm run dev:ssr

Open http://localhost:4200. You’ll see your app running with SSR.


Step 3: Pre-Rendering (Optional SSG)

For static sites, pre-render all routes at build time:

npm run prerender

This generates HTML files for specified routes (configured in angular.json).


17.8 Deployment Options

  • Node.js server: host on platforms like Heroku, AWS, or GCP.
  • Static hosting: pre-rendered output can be deployed to Netlify, Firebase, or GitHub Pages.
  • Hybrid: pre-render most pages, SSR dynamic ones.

17.9 Best Practices

  • Use TransferState to pass data from server to client, avoiding duplicate HTTP calls.
  • Be careful with browser-only APIs (localStorage, window). Use Angular’s isPlatformBrowser.
  • Pre-render as much as possible — fall back to SSR only when necessary.

17.10 Summary

  • Angular Universal enables Server-Side Rendering and Static Site Generation.
  • SSR improves SEO, performance, and user experience.
  • Setup is straightforward with ng add @nguniversal/express-engine.
  • Use hydration for seamless transitions between server-rendered and client-interactive states.

Perfect choice 🙌 — Angular testing can feel intimidating if presented only in theory. A practical, hands-on testing chapter will give your readers confidence by showing them how to actually write and run tests for components, services, and full applications.

Here’s a book-style draft for Chapter 18: Testing Angular Applications (Practical Guide).


Chapter 18: Testing Angular Applications

Writing tests is one of the most valuable investments in software development. Angular provides a rich testing ecosystem out of the box, using Jasmine (test framework) and Karma (test runner). Many teams also use Jest and Cypress for modern workflows.

In this chapter, we’ll focus on practical setups:

  • Unit testing components and services.
  • Testing HTTP requests.
  • Writing end-to-end (E2E) tests.
  • Tips for faster, maintainable testing.

18.1 Angular Testing Setup

When you generate a project with the Angular CLI, testing support is already included:

  • Jasmine for writing specs.
  • Karma for running tests in a browser.
  • Test files use the .spec.ts suffix.

To run tests:

ng test

18.2 Unit Testing a Service

Let’s start with a simple service.

Service:

@Injectable({ providedIn: 'root' })
export class CalculatorService {
  add(a: number, b: number) { return a + b; }
  subtract(a: number, b: number) { return a - b; }
}

Test:

import { TestBed } from '@angular/core/testing';
import { CalculatorService } from './calculator.service';

describe('CalculatorService', () => {
  let service: CalculatorService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(CalculatorService);
  });

  it('should add numbers', () => {
    expect(service.add(2, 3)).toBe(5);
  });

  it('should subtract numbers', () => {
    expect(service.subtract(5, 2)).toBe(3);
  });
});

18.3 Unit Testing a Component

Component:

@Component({
  selector: 'app-greeter',
  standalone: true,
  template: `<p>Hello {{ name }}</p>`
})
export class GreeterComponent {
  @Input() name = 'Angular';
}

Test:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GreeterComponent } from './greeter.component';

describe('GreeterComponent', () => {
  let fixture: ComponentFixture<GreeterComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [GreeterComponent]
    }).compileComponents();

    fixture = TestBed.createComponent(GreeterComponent);
  });

  it('should render default name', () => {
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toContain('Hello Angular');
  });

  it('should render input name', () => {
    fixture.componentInstance.name = 'World';
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toContain('Hello World');
  });
});

18.4 Testing Services with HTTP

Angular provides the HttpTestingController to mock HTTP requests.

Service:

@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(private http: HttpClient) {}
  getUsers() { return this.http.get<any[]>('/api/users'); }
}

Test:

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule]
    });
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  it('should fetch users', () => {
    const mockUsers = [{ name: 'Alice' }, { name: 'Bob' }];

    service.getUsers().subscribe(users => {
      expect(users).toEqual(mockUsers);
    });

    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('GET');
    req.flush(mockUsers);

    httpMock.verify();
  });
});

18.5 End-to-End Testing with Cypress

Angular CLI used to ship with Protractor, but most teams now use Cypress for E2E.

Install Cypress:

npm install cypress --save-dev

Add a test (cypress/e2e/app.cy.ts):

describe('My First Test', () => {
  it('visits the app root', () => {
    cy.visit('http://localhost:4200');
    cy.contains('Hello Angular');
  });
});

Run Cypress:

npx cypress open

This opens the Cypress test runner with a browser simulation.


18.6 Tips for Better Angular Tests

  • Use TestBed for components and services to get DI and Angular environment.
  • Keep unit tests small — one assertion per test is best.
  • Mock external APIs (use HttpTestingController, or libraries like jest-mock).
  • Use async/await in tests for better readability with asynchronous code.
  • Prefer Jest over Karma if you want faster, modern testing (drop-in replacement for Jasmine).
  • Test behavior, not implementation details.

18.7 Summary

  • Angular testing is built-in with Jasmine + Karma.
  • Use TestBed for unit testing components and services.
  • Mock HTTP with HttpTestingController.
  • For end-to-end tests, prefer Cypress over Protractor.
  • Keep tests small, focused, and meaningful.

Chapter 20: State Management with NgRx

NgRx is a state management library for Angular based on Redux principles: a single source of truth (the store), immutable state updates, and unidirectional data flow. It’s widely used in enterprise Angular applications.


20.1 NgRx Core Concepts

  1. Store – a single, centralized state tree for your app.
  2. Actions – plain objects that describe what happened.
  3. Reducers – pure functions that define how state changes.
  4. Selectors – functions to read pieces of state.
  5. Effects – handle side effects like HTTP calls.

20.2 Setting Up NgRx

Install the NgRx packages:

ng add @ngrx/store
ng add @ngrx/effects
ng add @ngrx/store-devtools

This sets up the store, devtools, and effects integration.


20.3 Building a Todo App (Local State)

We’ll build a Todo app with two components:

  • TodoFormComponent – adds new todos.
  • TodoListComponent – displays todos.

20.3.1 Define State and Model

todo.model.ts

export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

20.3.2 Define Actions

todo.actions.ts

import { createAction, props } from '@ngrx/store';
import { Todo } from './todo.model';

export const addTodo = createAction(
  '[Todo] Add Todo',
  props<{ title: string }>()
);

export const toggleTodo = createAction(
  '[Todo] Toggle Todo',
  props<{ id: number }>()
);

export const deleteTodo = createAction(
  '[Todo] Delete Todo',
  props<{ id: number }>()
);

20.3.3 Reducer

todo.reducer.ts

import { createReducer, on } from '@ngrx/store';
import { addTodo, toggleTodo, deleteTodo } from './todo.actions';
import { Todo } from './todo.model';

export const initialState: Todo[] = [];

let nextId = 1;

export const todoReducer = createReducer(
  initialState,
  on(addTodo, (state, { title }) => [
    ...state,
    { id: nextId++, title, completed: false }
  ]),
  on(toggleTodo, (state, { id }) =>
    state.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    )
  ),
  on(deleteTodo, (state, { id }) =>
    state.filter(todo => todo.id !== id)
  )
);

20.3.4 Selectors

todo.selectors.ts

import { createSelector, createFeatureSelector } from '@ngrx/store';
import { Todo } from './todo.model';

export const selectTodos = createFeatureSelector<Todo[]>('todos');

export const selectCompletedTodos = createSelector(
  selectTodos,
  todos => todos.filter(t => t.completed)
);

20.3.5 Register the Store

main.ts

import { bootstrapApplication } from '@angular/platform-browser';
import { provideStore } from '@ngrx/store';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { AppComponent } from './app/app.component';
import { todoReducer } from './app/todos/todo.reducer';

bootstrapApplication(AppComponent, {
  providers: [
    provideStore({ todos: todoReducer }),
    provideStoreDevtools()
  ]
});

20.3.6 Components

TodoFormComponent

import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { addTodo } from './todo.actions';

@Component({
  selector: 'app-todo-form',
  standalone: true,
  template: `
    <form (ngSubmit)="add()">
      <input [(ngModel)]="title" name="title" required>
      <button type="submit">Add</button>
    </form>
  `,
  imports: []
})
export class TodoFormComponent {
  title = '';

  constructor(private store: Store) {}

  add() {
    if (this.title.trim()) {
      this.store.dispatch(addTodo({ title: this.title }));
      this.title = '';
    }
  }
}

TodoListComponent

import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { Todo } from './todo.model';
import { selectTodos } from './todo.selectors';
import { toggleTodo, deleteTodo } from './todo.actions';
import { AsyncPipe, NgFor } from '@angular/common';

@Component({
  selector: 'app-todo-list',
  standalone: true,
  imports: [AsyncPipe, NgFor],
  template: `
    <ul>
      <li *ngFor="let todo of todos$ | async">
        <input type="checkbox"
          [checked]="todo.completed"
          (change)="toggle(todo.id)" />
        <span [style.text-decoration]="todo.completed ? 'line-through' : 'none'">
          {{ todo.title }}
        </span>
        <button (click)="remove(todo.id)">X</button>
      </li>
    </ul>
  `
})
export class TodoListComponent {
  todos$: Observable<Todo[]> = this.store.select(selectTodos);

  constructor(private store: Store) {}

  toggle(id: number) {
    this.store.dispatch(toggleTodo({ id }));
  }

  remove(id: number) {
    this.store.dispatch(deleteTodo({ id }));
  }
}

AppComponent

import { Component } from '@angular/core';
import { TodoFormComponent } from './todos/todo-form.component';
import { TodoListComponent } from './todos/todo-list.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TodoFormComponent, TodoListComponent],
  template: `
    <h1>Todo App (NgRx)</h1>
    <app-todo-form></app-todo-form>
    <app-todo-list></app-todo-list>
  `
})
export class AppComponent {}

20.4 Adding REST API with Effects

Now let’s persist todos via a REST API (e.g., https://jsonplaceholder.typicode.com).


20.4.1 Update Actions

todo.actions.ts

// load from API
export const loadTodos = createAction('[Todo] Load Todos');
export const loadTodosSuccess = createAction(
  '[Todo] Load Todos Success',
  props<{ todos: Todo[] }>()
);
export const loadTodosFailure = createAction(
  '[Todo] Load Todos Failure',
  props<{ error: any }>()
);

20.4.2 Effects

todo.effects.ts

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { HttpClient } from '@angular/common/http';
import { loadTodos, loadTodosSuccess, loadTodosFailure } from './todo.actions';
import { catchError, map, mergeMap, of } from 'rxjs';

@Injectable()
export class TodoEffects {
  constructor(private actions$: Actions, private http: HttpClient) {}

  loadTodos$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadTodos),
      mergeMap(() =>
        this.http.get<any[]>('https://jsonplaceholder.typicode.com/todos?_limit=5')
          .pipe(
            map(todos => loadTodosSuccess({ todos })),
            catchError(error => of(loadTodosFailure({ error })))
          )
      )
    )
  );
}

20.4.3 Register Effects

main.ts

import { provideEffects } from '@ngrx/effects';
import { TodoEffects } from './app/todos/todo.effects';
import { provideHttpClient } from '@angular/common/http';

bootstrapApplication(AppComponent, {
  providers: [
    provideStore({ todos: todoReducer }),
    provideEffects([TodoEffects]),
    provideHttpClient(),
    provideStoreDevtools()
  ]
});

20.4.4 Update Reducer

Handle API actions:

import { loadTodosSuccess } from './todo.actions';

export const todoReducer = createReducer(
  initialState,
  // ...
  on(loadTodosSuccess, (state, { todos }) => [...todos])
);

20.4.5 Trigger API Call in Component

@Component({
  // ...
})
export class TodoListComponent {
  todos$: Observable<Todo[]> = this.store.select(selectTodos);

  constructor(private store: Store) {
    this.store.dispatch(loadTodos()); // load on init
  }

  // toggle/remove unchanged
}

20.5 Summary

  • NgRx organizes state with actions, reducers, selectors, effects.
  • Built a Todo app with form and list using local NgRx state.
  • Extended it with REST API integration using HttpClient and Effects.
  • Store DevTools help debug and time-travel state changes.

20.6 NgRx vs Signals

With Angular v16+, we now have signals, a simpler reactivity model built into the framework. Developers often ask: Should I use Signals or NgRx? The answer depends on app complexity and team needs.


Signals Approach

How it works:

  • Use Angular signals (signal(), computed(), effect()) in services for local or shared state.
  • Changes propagate automatically to the template.
  • Simple, no external library required.

Example:

@Injectable({ providedIn: 'root' })
export class TodoStore {
  todos = signal<Todo[]>([]);

  add(title: string) {
    this.todos.update(list => [
      ...list,
      { id: Date.now(), title, completed: false }
    ]);
  }

  toggle(id: number) {
    this.todos.update(list =>
      list.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  }
}

Pros:

  • ✅ Built-in, no extra library.
  • ✅ Very easy to learn.
  • ✅ Fine-grained reactivity (updates only what changes).
  • ✅ Great for small to medium apps or feature modules.

Cons:

  • ❌ No built-in devtools/time-travel debugging.
  • ❌ No formalized patterns (every team may structure differently).
  • ❌ For large teams, scaling can lead to “DIY state management.”

NgRx Approach

How it works:

  • Centralized store that holds global state.
  • State changes only through actions and reducers.
  • Selectors read state; effects handle side effects.

Pros:

  • ✅ Predictable, standardized patterns.
  • ✅ Excellent for large/enterprise apps.
  • ✅ Rich ecosystem: NgRx Store, Effects, Entity, Router Store.
  • ✅ Powerful DevTools (time-travel, action replay).

Cons:

  • ❌ Verbose boilerplate (actions, reducers, effects).
  • ❌ Steeper learning curve.
  • ❌ Can feel “overkill” for smaller apps.

Side-by-Side Comparison

FeatureSignalsNgRx
Setup ComplexityMinimal, built-inHigh (multiple files: actions, reducers)
Learning CurveLow (just signals API)Medium–High (Redux patterns)
Best ForLocal/feature state, small appsGlobal state, enterprise-scale apps
Debugging ToolsConsole/logging onlyNgRx DevTools (time-travel, inspection)
Side Effects HandlingServices + async/awaitNgRx Effects (powerful, testable)
BoilerplateVery lowHigh
Team ConsistencyDepends on conventionsEnforced by NgRx structure

Suggested Guidelines

  • ✅ Use Signals when:

    • You’re building a small or medium app.
    • You just need reactive state for a feature (e.g., form, cart).
    • You prefer simplicity and less boilerplate.
  • ✅ Use NgRx when:

    • You’re working on a large, multi-team enterprise app.
    • State is global and complex (auth, caching, offline sync).
    • You need strict predictability and debugging tools.

20.7 Summary

  • Signals are lightweight, local, reactive state.
  • NgRx is enterprise-scale, centralized state management.
  • They are not mutually exclusive — you can use signals for feature-level state and NgRx for global app state in the same project.