Angular

Angular is a full application framework built on TypeScript. Unlike React or Vue, it includes everything: routing, forms, HTTP, dependency injection, and a CLI — all from the same team with the same conventions.


🟢 Junior

Components

A component is a class decorated with @Component. It binds a TypeScript class to an HTML template.

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

@Component({
  selector: 'app-greeting',
  standalone: true,
  imports: [CommonModule],
  template: `
    <h1>Hello, !</h1>
    <button (click)="count++">Clicked 8 times</button>
    <ul>
      <li *ngFor="let item of items; trackBy: trackById"></li>
    </ul>
  `
})
export class GreetingComponent {
  name = 'World';
  count = 0;
  items = [{ id: 1, name: 'Alpha' }, { id: 2, name: 'Beta' }];

  trackById(index: number, item: { id: number }) {
    return item.id;
  }
}

Standalone components (Angular 14+) don’t need an NgModule. They import their own dependencies.

Template Syntax

`` — interpolation, string output. [property]="expression" — property binding, sets a DOM property. (event)="handler($event)" — event binding. [(ngModel)]="field" — two-way binding (requires FormsModule). *ngIf="condition" — structural directive, adds/removes element. *ngFor="let x of list" — structural directive, repeats element.

Services and Dependency Injection Basics

A service is a class that holds shared logic or data. Decorate it with @Injectable so Angular’s DI system can manage it.

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

@Injectable({ providedIn: 'root' }) // singleton in the root injector
export class UserService {
  private users: User[] = [];

  getAll(): User[] { return this.users; }

  add(user: User): void { this.users.push(user); }
}

Inject into a component via the constructor:

@Component({ ... })
export class UserListComponent {
  constructor(private userService: UserService) {}

  get users() { return this.userService.getAll(); }
}

🟡 Medior

RxJS and HttpClient

Angular’s HttpClient returns Observable, not Promises. Observables are lazy — they don’t execute until subscribed.

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

@Injectable({ providedIn: 'root' })
export class PostService {
  constructor(private http: HttpClient) {}

  getPosts(): Observable<Post[]> {
    return this.http.get<Post[]>('/api/posts').pipe(
      map(posts => posts.filter(p => p.published)),
      catchError(err => throwError(() => new Error(`Failed: ${err.status}`)))
    );
  }
}

In a component, subscribe using the async pipe to avoid manual subscription management (it auto-unsubscribes on destroy):

@Component({
  template: `
    <div *ngFor="let post of posts$ | async"></div>
    <div *ngIf="error$ | async as err" class="error"></div>
  `
})
export class PostListComponent {
  posts$ = this.postService.getPosts();
  constructor(private postService: PostService) {}
}

Reactive Forms

Reactive forms define the form structure in TypeScript, giving full control and testability.

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

@Component({
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="submit()">
      <input formControlName="email" type="email" />
      <span *ngIf="form.get('email')?.errors?.['email']">Invalid email</span>
      <input formControlName="password" type="password" />
      <button [disabled]="form.invalid">Submit</button>
    </form>
  `
})
export class LoginComponent {
  form = inject(FormBuilder).nonNullable.group({
    email:    ['', [Validators.required, Validators.email]],
    password: ['', [Validators.required, Validators.minLength(8)]]
  });

  submit() {
    if (this.form.valid) {
      console.log(this.form.getRawValue());
    }
  }
}

Routing and Lazy Loading

Routes map URL patterns to components. Lazy loading splits the bundle so route modules are only loaded when the user navigates there.

export const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'users',
    loadChildren: () => import('./users/users.routes').then(m => m.USER_ROUTES)
  },
  {
    path: 'admin',
    canActivate: [AuthGuard],
    loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)
  },
  { path: '**', redirectTo: '' }
];

Route guards (canActivate, canDeactivate, resolve) run before navigation completes. They can return booleans, UrlTrees (redirects), or Observables/Promises of either.

Change Detection and OnPush

Angular’s default change detection checks every component on every browser event. OnPush restricts checks to: input reference changes, events originating from the component, async pipe emissions, and explicit markForCheck() calls.

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: ``
})
export class UserCardComponent {
  @Input() user!: User;
}

OnPush components are significantly faster in large trees. Pair with immutable data patterns (always create new object references on update) to work correctly.


🔴 Senior

Signals (Angular 16+)

Signals are Angular’s new reactive primitive — a synchronous, pull-based alternative to RxJS for component state.

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

const count     = signal(0);
const doubled   = computed(() => count() * 2); // reactive derived value
const formatted = computed(() => `Count is ${count()}`);

effect(() => {
  console.log(`Count changed to: ${count()}`);
});

count.set(1);      // logs: "Count changed to: 1"
count.update(c => c + 1);  // logs: "Count changed to: 2"

In templates, call the signal like a function: 8. Angular knows to re-render only the parts that depend on the changed signal.

toSignal converts an Observable to a signal. toObservable does the reverse.

Dependency Injection — Advanced

The injector hierarchy allows scoping services to specific components or routes:

@Component({
  providers: [CartService] // new instance for this component and its subtree
})
export class CartComponent { ... }

inject() is the functional alternative to constructor injection, usable in class fields and functions called from within an injection context:

@Component({ ... })
export class MyComponent {
  private http    = inject(HttpClient);
  private router  = inject(Router);
  private destroy = inject(DestroyRef); // auto-cleanup

  ngOnInit() {
    this.http.get('/api/data').pipe(
      takeUntilDestroyed(this.destroy)
    ).subscribe(data => { ... });
  }
}

Custom Structural Directives

Structural directives manipulate the DOM by adding or removing elements. *ngIf is a structural directive.

@Directive({ selector: '[appFeatureFlag]', standalone: true })
export class FeatureFlagDirective implements OnInit {
  @Input('appFeatureFlag') flag!: string;

  constructor(
    private templateRef: TemplateRef<unknown>,
    private viewContainer: ViewContainerRef,
    private featureService: FeatureService
  ) {}

  ngOnInit() {
    if (this.featureService.isEnabled(this.flag)) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    }
  }
}

Usage: <div *appFeatureFlag="'dark-mode'">...</div>

Performance at Scale

Track functions in ngFor: Always provide trackBy for lists. Without it, Angular destroys and recreates every DOM node on each render, even for unchanged items.

Virtual scrolling: @angular/cdk/scrolling with cdk-virtual-scroll-viewport renders only visible rows — essential for lists with thousands of items.

Bundle optimization: Use loadChildren (lazy-loaded route modules) and loadComponent (standalone lazy components). Analyze with webpack-bundle-analyzer or Angular’s ng build --stats-json.

Pure pipes: Pipes with pure: true (the default) are only re-executed when inputs change by reference — equivalent to useMemo in React.

Senior Gotchas

Manual subscriptions that aren’t unsubscribed cause memory leaks. Use takeUntilDestroyed(this.destroyRef), the async pipe, or DestroyRef.onDestroy() to ensure cleanup.

ngZone.runOutsideAngular() runs code without triggering change detection — useful for high-frequency events like scroll listeners or WebSocket messages that don’t affect the template.

Modifying a binding after it has been checked throws ExpressionChangedAfterItHasBeenCheckedError in dev mode. This usually means you have a lifecycle hook that changes something it shouldn’t. Fix by restructuring the data flow or using setTimeout / Promise.resolve() to defer the change (a code smell — prefer redesigning).

Avoid direct DOM access (document.querySelector, element.nativeElement) in components. Use @ViewChild, Renderer2, or the CDK for testable, cross-platform DOM interactions.