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:
- An @Input reference changes (new object/array reference, not deep mutation).
- An event originates inside the component or its children.
- An async pipe receives a new value.
- 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/trackon all lists.- OnPush + immutable updates on hot paths.
@deferblocks to delay heavy component rendering until visible.- Lazy routes and preloading strategies (
PreloadAllModulesor 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.