AngularAdvanced

Advanced Angular: Change Detection, State & Production Patterns

Optimize rendering with OnPush change detection, intercept HTTP traffic globally, manage application state with NgRx or lightweight alternatives, improve performance with trackBy and lazy strategies, test with TestBed, and understand SSR with hydration.

4 sections · ~35 min · 5-question quiz (pass ≥ 70%)

1Change Detection: Default vs OnPush

Angular checks components for template updates through change detection. By default (ChangeDetectionStrategy.Default), every async event (click, HTTP response, timer) triggers a check of the entire component tree from root to leaves.

OnPush narrows that scope dramatically:

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  /* ... */
})
export class UserRowComponent {
  user = input.required<User>(); // signal input — OnPush-friendly
}

With OnPush, Angular re-renders a component only when:

  1. An @Input reference changes (new object/array reference, not deep mutation).
  2. An event originates inside the component or its children.
  3. An async pipe receives a new value.
  4. A signal read in the template changes.

Practical impact: OnPush + immutable data patterns (spread to create new objects) cut unnecessary DOM work in large tables and dashboards. Avoid mutating this.user.name = 'x' in place when the parent passes user — create { ...user, name: 'x' } instead. ChangeDetectorRef.markForCheck() manually schedules a check when you update state outside Angular's zone (rare with signals and async pipe).

2HTTP Interceptors: Cross-Cutting Concerns

Interceptors sit in the HttpClient pipeline and transform every outgoing request or incoming response. Register them with provideHttpClient(withInterceptors([...])) (functional interceptors, Angular 15+) or the class-based HTTP_INTERCEPTORS token.

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).token();
  if (token) {
    req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
  }
  return next(req);
};

export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  return next(req).pipe(
    catchError(err => {
      if (err.status === 401) inject(Router).navigate(['/login']);
      return throwError(() => err);
    })
  );
};

Typical interceptor responsibilities:

  • Attach auth tokens and correlation IDs.
  • Add base URLs or API versioning headers.
  • Retry idempotent GET requests on transient failures.
  • Normalize errors into a consistent shape for the UI.
  • Show/hide a global loading spinner via a shared service.

Keep interceptors thin — heavy business logic belongs in services. Order matters: auth runs before logging; error handling usually runs last in the chain.

3State Management: NgRx and Lightweight Alternatives

Large Angular apps centralize state to avoid prop-drilling and inconsistent caches. NgRx (Redux pattern) is the enterprise standard:

// Actions describe events
export const loadUsers = createAction('[Users] Load');
export const loadUsersSuccess = createAction('[Users] Load Success', props<{ users: User[] }>());

// Reducer — pure function (state, action) => newState
export const usersReducer = createReducer(
  initialState,
  on(loadUsersSuccess, (state, { users }) => ({ ...state, users, loading: false }))
);

// Effects — side effects (HTTP) triggered by actions
loadUsers$ = createEffect(() =>
  this.actions$.pipe(
    ofType(loadUsers),
    switchMap(() => this.api.getUsers().pipe(
      map(users => loadUsersSuccess({ users }))
    ))
  )
);

// Selectors — memoized queries
selectActiveUsers = createSelector(selectAllUsers, users => users.filter(u => u.active));

When to use NgRx: Many features share the same data, you need time-travel debugging, or audit requirements demand explicit action logs. When to skip it: Small apps where BehaviorSubject in a service, ComponentStore, or signals + inject() suffice. NgRx SignalStore (NgRx 17+) blends signals with the store pattern for less boilerplate. Whatever you choose, treat server state (HTTP) and UI state (modal open, selected tab) differently — libraries like TanStack Query patterns map well to Angular services with signals.

4Performance, Testing, SSR & Hydration

Performance checklist:

  • trackBy / track on all lists.
  • OnPush + immutable updates on hot paths.
  • @defer blocks to delay heavy component rendering until visible.
  • Lazy routes and preloading strategies (PreloadAllModules or custom).
  • Avoid expensive work in template expressions — use computed() or pipes with pure:true.

Testing with TestBed:

describe('UserListComponent', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [UserListComponent],
      providers: [{ provide: UserService, useValue: { getUsers: () => of([mockUser]) } }],
    }).compileComponents();
  });

  it('renders users', () => {
    const fixture = TestBed.createComponent(UserListComponent);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toContain('Ada');
  });
});

TestBed creates an Angular testing module, compiles the component, and runs change detection via detectChanges(). Mock services with useValue or useClass. Use HttpClientTestingModule to assert HTTP calls without hitting the network.

SSR & hydration: Angular Universal renders HTML on the server for SEO and first-contentful paint. Hydration (Angular 16+) reuses server-rendered DOM instead of destroying and re-creating it in the browser — faster and flicker-free. Enable with provideClientHydration(). Avoid direct DOM manipulation that mismatches server output; use Angular bindings consistently.

Ready to test yourself?

Sign in to take the quiz, track progress, and earn a certificate.

Sign in