Angular Advanced — Signals, Performance & Pro Patterns
Angular advanced patterns help you build performant, scalable applications. Learn Signals for reactive state, optimize change detection, create dynamic UIs, add animations, and implement server-side rendering.
What You’ll Learn
- Use Signals (
signal,computed,effect) for reactive state - Optimize performance with
OnPushchange detection - Build flexible components with content projection
- Create dynamic components at runtime
- Add animations with Angular’s animation DSL
- Build custom directives and pipes
- Implement SSR with Angular Universal
Why Advanced Angular Matters
Performance separates good apps from great ones. Doda Browser’s settings panel uses OnPush change detection to stay responsive across hundreds of nested components. Durga Antivirus Pro uses Signals for real-time threat notifications without unnecessary re-renders. These advanced patterns ensure your apps handle complexity without slowing down.
flowchart LR
A["Angular HTTP & Services"] --> B["**Angular Advanced**"]
B --> C["Angular Reference & Real Projects"]
style B fill:#f97316,stroke:#c2410c,color:#fff
Signals — Reactive State Made Simple
Signals are Angular’s reactive primitive. Think of them as reactive variables — when a signal’s value changes, anything that depends on it automatically updates.
flowchart TD
A[Angular Advanced] --> B[Signals]
A --> C[Change Detection]
A --> D[Content Projection]
A --> E[Dynamic Components]
A --> F[View Encapsulation]
A --> G[Animations]
A --> H[Custom Directives]
A --> I[Custom Pipes]
A --> J[SSR & Hydration]
B --> K[signal / computed / effect]
import { signal, computed, effect } from "@angular/core";
// Create a signal with an initial value
const count = signal(0);
const name = signal("Alice");
// Read a signal — call it like a function
console.log(count()); // 0
// Update a signal
count.set(5); // Replace the value
count.update(v => v + 1); // Derive new value from current
// Computed — derived signals (read-only, auto-updating)
const double = computed(() => count() * 2);
// Effect — runs side effects whenever its dependencies change
effect(() => {
console.log(`Count changed to ${count()}`);
});Why Signals instead of a plain variable? With a plain variable, Angular doesn’t know when it changes — it has to check the entire component tree. With Signals, Angular knows exactly what changed, and only updates the relevant parts of the DOM.
Signals in Components
import { Component, signal, computed } from "@angular/core";
@Component({
selector: "app-counter",
standalone: true,
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ double() }}</p>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
count = signal(0);
double = computed(() => this.count() * 2);
increment() {
this.count.update(v => v + 1);
}
}Notice we call count() with parentheses in the template. That’s because signals are functions — calling them returns the current value and also tracks them as dependencies.
Signal Inputs and Outputs (Angular 17+)
import { Component, input, output } from "@angular/core";
@Component({
selector: "app-greeting",
standalone: true,
template: `<h1>Hello, {{ name() }}!</h1>`
})
export class GreetingComponent {
name = input.required<string>(); // Required input
age = input(0); // Optional with default
greeted = output<string>(); // Output (replaces EventEmitter)
greet() {
this.greeted.emit(this.name());
}
}input() replaces @Input() and output() replaces @Output() + EventEmitter. Signal inputs are more type-safe and integrate perfectly with Angular’s change detection.
Change Detection Strategies
Change detection is how Angular knows to update the UI when data changes.
Default Strategy
Angular checks the entire component tree whenever anything changes. For most apps, this is fast enough. But for large component trees (1000+ nodes), it can cause jank.
OnPush Strategy
OnPush only checks a component when:
- An
@Inputproperty gets a new reference - An event (click, input, etc.) fires inside the component
- A bound Observable emits a new value
markForCheck()is called manually
import { Component, ChangeDetectionStrategy, Input } from "@angular/core";
@Component({
selector: "app-expensive-list",
standalone: true,
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush // 👈 Switch to OnPush
})
export class ExpensiveListComponent {
@Input() items: any[] = [];
}The critical rule for OnPush: Mutating an array (push, splice, index assignment) won’t trigger change detection because the reference hasn’t changed. Always create a new reference:
// ❌ Won't trigger OnPush
this.items.push(newItem);
// ✅ Triggers OnPush (new array reference)
this.items = [...this.items, newItem];| Strategy | When to Use |
|---|---|
| Default | Small to medium apps, simple components |
| OnPush | Large component trees, list components, performance-critical sections |
Content Projection
Content projection lets you create flexible, reusable components. Instead of the parent controlling everything, the child defines slots where the parent injects content:
// card.component.ts
import { Component } from "@angular/core";
@Component({
selector: "app-card",
standalone: true,
template: `
<div class="card">
<div class="card-header">
<ng-content select="[card-header]" />
</div>
<div class="card-body">
<ng-content /> <!-- Default slot -->
</div>
<div class="card-footer">
<ng-content select="[card-footer]" />
</div>
</div>
`,
styles: [`
.card { border: 1px solid #ddd; border-radius: 8px; padding: 16px; }
.card-header { font-weight: bold; margin-bottom: 8px; }
.card-footer { margin-top: 8px; border-top: 1px solid #eee; padding-top: 8px; }
`]
})
export class CardComponent {}<!-- Parent usage -->
<app-card>
<div card-header>My Card Title</div>
<p>This is the body content — goes to the default slot.</p>
<div card-footer>
<button>Save</button>
</div>
</app-card>Think of <ng-content> as a slot where content from the parent gets inserted. The select attribute lets you target specific elements by CSS selector, creating named slots.
Dynamic Components
Sometimes you need to create components at runtime — like notifications, modals, or tooltips:
import { Component, ViewChild, ViewContainerRef, ComponentRef } from "@angular/core";
import { AlertComponent } from "./alert/alert.component";
@Component({
selector: "app-dynamic",
standalone: true,
imports: [AlertComponent],
template: `
<button (click)="showAlert()">Show Alert</button>
<ng-template #alertContainer />
`
})
export class DynamicComponent {
@ViewChild("alertContainer", { read: ViewContainerRef }) container!: ViewContainerRef;
showAlert() {
this.container.clear(); // Remove previous alerts
const ref: ComponentRef<AlertComponent> =
this.container.createComponent(AlertComponent);
ref.instance.message = "This is a dynamic alert!";
ref.instance.close.subscribe(() => ref.destroy()); // Clean up
}
}ViewContainerRef— A reference to a location in the view where you can insert components.createComponent()— Creates a component instance and adds it to the DOM.ref.instance— Access the component’s public API to set inputs and listen to outputs.ref.destroy()— You must manually destroy dynamic components to prevent memory leaks.
Animations
Angular’s animation system lets you define transitions declaratively:
import { Component } from "@angular/core";
import { trigger, state, style, animate, transition } from "@angular/animations";
@Component({
selector: "app-fade",
standalone: true,
animations: [
trigger("fadeInOut", [
state("void", style({ opacity: 0, transform: "translateY(-10px)" })),
transition(":enter, :leave", [
animate("300ms ease-in-out")
])
])
],
template: `
<button (click)="show = !show">Toggle</button>
@if (show) {
<div @fadeInOut style="padding: 20px; background: #f0f0f0; border-radius: 8px;">
Animated content
</div>
}
`
})
export class FadeComponent {
show = false;
}trigger("fadeInOut")— Names the animation. Use@fadeInOutin the template.state("void", ...)— Defines the starting style (element not yet in DOM).:enterand:leave— Special aliases for when an element is added/removed.animate("300ms ease-in-out")— Duration and easing function.
provideAnimations() in your app config to enable them.Custom Directives
Directives let you extend HTML with custom behavior. Here’s a directive that highlights elements on hover:
import { Directive, ElementRef, HostListener, Input } from "@angular/core";
@Directive({
selector: "[appHighlight]",
standalone: true
})
export class HighlightDirective {
@Input("appHighlight") color: string = "yellow";
constructor(private el: ElementRef) {}
@HostListener("mouseenter") onMouseEnter() {
this.el.nativeElement.style.background = this.color;
}
@HostListener("mouseleave") onMouseLeave() {
this.el.nativeElement.style.background = "";
}
}
// Usage: <p appHighlight="lightgreen">Hover me</p>
ElementRef— Gives access to the host DOM element.@HostListener— Listens for events on the host element.@Input("appHighlight")— Allows passing a color value via the directive attribute.
Custom Pipes
Pipes transform data in templates. Here’s a pipe that truncates long text:
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
name: "truncate",
standalone: true
})
export class TruncatePipe implements PipeTransform {
transform(value: string, maxLength: number = 50, suffix: string = "..."): string {
return value.length > maxLength
? value.substring(0, maxLength) + suffix
: value;
}
}
// Usage: <p>{{ longText | truncate:100 }}</p>
Every pipe must implement PipeTransform with a transform method. The first argument is the input value. Additional arguments come after colons in the template.
SSR & Hydration
Server-Side Rendering (SSR) pre-renders your Angular app on the server, sending HTML to the browser for faster initial loads and better SEO:
ng add @angular/ssr
npm run dev:ssr # Development with SSR
npm run build:ssr # Production buildHydration reuses the server-rendered DOM instead of rebuilding it:
// app.config.ts
import { provideClientHydration } from "@angular/platform-browser";
providers: [
provideClientHydration()
]SSR Gotchas
// ❌ This crashes on the server — window doesn't exist
const width = window.innerWidth;
// ✅ Guard browser APIs
import { isPlatformBrowser } from "@angular/common";
import { inject, PLATFORM_ID } from "@angular/core";
const platformId = inject(PLATFORM_ID);
if (isPlatformBrowser(platformId)) {
const width = window.innerWidth; // Safe now
}window, document, and localStorage don’t exist on the server. Always guard browser-specific code.
State Management Overview
| Approach | Use Case |
|---|---|
| Signals | Component-local state, simple shared state |
| Service + BehaviorSubject | Shared state across components |
| NgRx | Complex state with actions, reducers, effects |
| NgRx Component Store | Local component state management |
Simple State Management with BehaviorSubject
@Injectable({ providedIn: "root" })
export class CartService {
private itemsSubject = new BehaviorSubject<Product[]>([]);
items$ = this.itemsSubject.asObservable();
addItem(product: Product) {
const current = this.itemsSubject.value;
this.itemsSubject.next([...current, product]);
}
removeItem(id: number) {
this.itemsSubject.next(
this.itemsSubject.value.filter(i => i.id !== id)
);
}
}BehaviorSubject holds a current value and emits updates. Components subscribe to items$ and get the latest value immediately.
Common Mistakes
1. Overusing OnPush without understanding reference changes
OnPush only checks for new references. Mutating an array with push won’t trigger change detection. Always create new references.
2. Using signals when RxJS is better for streams
Signals are for synchronous state (form values, toggles, counters). For event streams (HTTP, WebSocket, debounced input), RxJS remains the right choice.
3. Not cleaning up dynamic components
Dynamic components created with ViewContainerRef.createComponent() must be manually destroyed. Failing to do so causes memory leaks.
4. Putting browser-specific code in SSR without guards
window, document, localStorage don’t exist on the server. Always guard with isPlatformBrowser.
5. Animations not imported as a feature
Angular animations are in a separate @angular/animations package. Import provideAnimations() in your app config.
6. Creating large component trees without OnPush
On large pages, the default change detection strategy can cause frame drops. Profile your app and selectively apply OnPush.
Practice Questions
What is a Signal in Angular? A reactive primitive that wraps a value and notifies consumers when it changes. Signals integrate with Angular’s change detection to update only the parts of the DOM that depend on them.
When should you use OnPush change detection? For components that receive immutable inputs and don’t need checking on every change. It improves performance in large component trees.
What is content projection? A pattern where a parent component injects content into a child component using
<ng-content>. Useselectattribute for multiple named slots.How do dynamic components differ from components loaded by the Router? Dynamic components are created at runtime using
ViewContainerRef.createComponent(). Router components are mapped by the Angular Router based on URL.What does
provideClientHydration()do? It enables hydration, which reuses the server-rendered DOM on the client instead of rebuilding it. This improves perceived performance for SSR apps.
Challenge
Build a notification system using Signals: a NotificationService with a signal<Notification[]> and methods show() + dismiss(), and a component that displays notifications in a fixed overlay with auto-dismiss after 4 seconds.
FAQ
Try It Yourself
Build a dynamic notification system using Signals:
// notification.service.ts
import { Injectable, signal } from "@angular/core";
export interface Notification {
id: number;
message: string;
type: "success" | "error" | "info";
}
@Injectable({ providedIn: "root" })
export class NotificationService {
private notifications = signal<Notification[]>([]);
readonly all = this.notifications.asReadonly();
private nextId = 1;
show(message: string, type: Notification["type"] = "info") {
const id = this.nextId++;
this.notifications.update(n => [...n, { id, message, type }]);
setTimeout(() => this.dismiss(id), 4000); // Auto-dismiss
}
dismiss(id: number) {
this.notifications.update(n => n.filter(item => item.id !== id));
}
}// app.component.ts — usage
notificationService.show("Saved successfully!", "success");
notificationService.show("Network error!", "error");What’s Next
| Tutorial | Description |
|---|---|
| https://tutorials.dodatech.com/frameworks/angular/reference/ | Quick API reference and cheatsheet |
| https://tutorials.dodatech.com/frameworks/angular/angular-http/ | HTTP Client and services deep dive |
| https://tutorials.dodatech.com/frameworks/angular/material/ | Angular Material UI component library |
Related topics: RxJS, TypeScript, Design Patterns.
What’s Next
Congratulations on completing this Angular Advanced tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro