Skip to main content

Angular Interview - Whiteboard Questions

Core Concepts

1. Standalone Components Architecture

Question: "Draw the architecture of a standalone Angular application and explain how it differs from NgModule-based apps."

Expected Answer:

┌─────────────────────────────────────┐
│ main.ts │
│ bootstrapApplication(AppComponent) │
│ providers: [ │
│ provideRouter(routes), │
│ provideHttpClient() │
│ ] │
└──────────────┬──────────────────────┘


┌──────────────────────────────────────┐
│ App Component (standalone) │
│ imports: [ │
│ HeaderComponent, │
│ RouterOutlet │
│ ] │
└──────────┬───────────────────────────┘


┌──────┴──────┐
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ Feature │ │ Feature │
│ Route 1 │ │ Route 2 │
│(lazy) │ │(lazy) │
└─────────┘ └─────────┘

Key Points:

  • No NgModules needed
  • Direct component imports
  • Provider functions for services
  • Lazy loading with loadComponent
  • Better tree-shaking

2. Signals Reactivity Model

Question: "Explain how Angular Signals work and diagram the reactivity flow."

Expected Answer:

signal(value)

[Read: count()]

computed(() => count() * 2) ← Automatic dependency tracking

effect(() => { ← Runs when dependencies change
console.log(count())
})

Template updates ← Fine-grained DOM updates

Key Points:

  • Pull-based reactivity (vs push-based RxJS)
  • Synchronous updates
  • Automatic dependency tracking
  • Memoization in computed()
  • Direct DOM updates without Zone.js

3. Change Detection Strategies

Question: "Diagram the change detection flow for Default, OnPush, and Signals."

Expected Answer:

Default Strategy:
Event → Zone.js → Check ALL components → Update DOM

OnPush Strategy:
Event → Zone.js → Check ONLY if:
- Input changed (reference)
- Component event
- Async pipe
- Manual trigger

Signals Strategy:
Signal.set() → Update ONLY affected bindings → Update DOM
(No Zone.js check needed!)

Performance Comparison:

1000 components, 1 change:

Default: Check 1000 components (~50ms)
OnPush: Check 1 component (~1ms)
Signals: Update 1 binding (~0.1ms)

4. Signals vs RxJS

Question: "When would you use Signals vs RxJS? Provide examples."

Expected Answer:

Use CaseSolutionWhy
Component stateSignalsSynchronous, simple, local
HTTP requestsRxJSAsync, cancellation, retry
Form validationSignalsImmediate feedback
WebSocket streamsRxJSContinuous async data
Computed valuesSignalsAuto-tracking, memoized
Complex operatorsRxJSdebounce, merge, switchMap

Example:

// ✅ Signals for local state
count = signal(0);
doubled = computed(() => this.count() * 2);

// ✅ RxJS for async streams
users$ = this.http.get<User[]>('/api/users').pipe(
retry(3),
catchError(err => of([]))
);

// ✅ Combine both
users = toSignal(this.users$, { initialValue: [] });

5. New Control Flow

Question: "Show the new control flow syntax and explain its benefits."

Expected Answer:

<!-- @if with else -->
@if (isLoggedIn) {
<dashboard />
} @else {
<login />
}

<!-- @for with track -->
@for (item of items; track item.id) {
<item-card [item]="item" />
} @empty {
<no-items />
}

<!-- @defer for lazy loading -->
@defer (on viewport) {
<heavy-chart />
} @loading {
<spinner />
} @placeholder {
<chart-skeleton />
}

<!-- @switch -->
@switch (status) { @case ('success') { <success-msg /> } @case ('error') {
<error-msg /> } @default { <pending-msg /> } }

Benefits:

  • Built-in, no CommonModule needed
  • Better TypeScript integration
  • Cleaner syntax
  • Performance optimized
  • @defer enables declarative lazy loading

6. Dependency Injection Hierarchy

Question: "Diagram Angular's DI hierarchy in a standalone app."

Expected Answer:

┌─────────────────────────────────┐
│ Root Injector (main.ts) │
│ provideHttpClient() │
│ Injectable({ providedIn: 'root' })
└──────────────┬──────────────────┘

┌──────────┴──────────┐
▼ ▼
┌─────────┐ ┌─────────┐
│Component│ │Component│
│providers│ │providers│
│[Service]│ │[Service]│
└─────────┘ └─────────┘
↓ ↓
Local scope Local scope

Key Points:

  • Root services: providedIn: 'root'
  • Environment injectors: bootstrapApplication providers
  • Component-level: providers: [] in component
  • inject() function for functional DI

7. Lazy Loading Strategy

Question: "Design a lazy loading strategy with preloading."

Expected Answer:

// Critical path - load immediately
{ path: '', component: HomeComponent }
{ path: 'dashboard', component: DashboardComponent }

// Important - preload
{
path: 'analytics',
data: { preload: true },
loadComponent: () => import('./analytics.component')
}

// On-demand only
{
path: 'admin',
data: { preload: false },
loadComponent: () => import('./admin.component')
}

// Preloading strategy
class SelectivePreload implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>) {
return route.data?.['preload'] ? load() : of(null);
}
}

8. State Management Architecture

Question: "Design a scalable state management architecture using Signals."

Expected Answer:

// Component Store Pattern
@Injectable()
class UserStore {
// Private state
private users = signal<User[]>([]);
private selectedId = signal<string | null>(null);
private loading = signal(false);

// Public selectors
readonly users$ = this.users.asReadonly();
readonly loading$ = this.loading.asReadonly();

readonly selectedUser = computed(() => {
const id = this.selectedId();
return this.users().find(u => u.id === id) ?? null;
});

// Actions
loadUsers() {
this.loading.set(true);
this.http.get<User[]>('/api/users').subscribe(users => {
this.users.set(users);
this.loading.set(false);
});
}

selectUser(id: string) {
this.selectedId.set(id);
}
}

9. Performance Optimization Strategy

Question: "List and explain 5 performance optimizations for Angular apps."

Expected Answer:

  1. OnPush Change Detection

    @Component({ changeDetection: ChangeDetectionStrategy.OnPush })
  2. Lazy Loading Routes

    loadComponent: () => import('./feature.component');
  3. NgOptimizedImage

    <img ngSrc="hero.jpg" width="1200" height="600" priority />
  4. @defer for Heavy Components

    @defer (on viewport) { <chart /> }
  5. TrackBy in Lists

    @for (item of items; track item.id) { }
  6. Signals for Fine-Grained Updates

    count = signal(0); // Only updates affected bindings

10. Zoneless Architecture

Question: "Explain zoneless change detection and how to enable it."

Expected Answer:

// main.ts
import { provideExperimentalZonelessChangeDetection } from '@angular/core';

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

How it works:

Traditional:
Event → Zone.js patches → Run change detection → Update DOM

Zoneless with Signals:
Signal.set() → Direct reactive update → Update DOM
(No Zone.js overhead!)

Requirements:

  • Use signals for reactivity
  • Use OnPush strategy
  • Manual CD for non-signal cases
  • Benefits: Predictable, faster, smaller bundles


Code Review & Bad Practices

11. RxJS - Memory Leaks

Question: "Identify the memory leak and fix it."

❌ Bad Practice:

@Component({
selector: 'app-user-list',
standalone: true,
template: `
@for (user of users; track user.id) {
<div>{{ user.name }}</div>
}
`,
})
export class UserListComponent implements OnInit {
users: User[] = [];

constructor(private userService: UserService) {}

ngOnInit() {
// ❌ PROBLEM: Subscription never unsubscribed - memory leak!
this.userService.getUsers().subscribe(users => {
this.users = users;
});

// ❌ PROBLEM: Interval keeps running after component destroyed
interval(1000).subscribe(() => {
console.log('Polling...');
});
}
}

✅ Fix Option 1: AsyncPipe (Recommended)

@Component({
selector: 'app-user-list',
standalone: true,
imports: [AsyncPipe],
template: `
@for (user of users$ | async; track user.id) {
<div>{{ user.name }}</div>
}
`,
})
export class UserListComponent {
// ✅ AsyncPipe handles subscription/unsubscription automatically
users$ = inject(UserService).getUsers();
}

✅ Fix Option 2: takeUntilDestroyed (Angular 16+)

@Component({
selector: 'app-user-list',
standalone: true,
})
export class UserListComponent implements OnInit {
users: User[] = [];
private destroyRef = inject(DestroyRef);

ngOnInit() {
// ✅ Automatically unsubscribes when component destroys
this.userService
.getUsers()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(users => (this.users = users));

interval(1000)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => console.log('Polling...'));
}
}

✅ Fix Option 3: Manual Cleanup

export class UserListComponent implements OnInit, OnDestroy {
users: User[] = [];
private subscriptions = new Subscription();

ngOnInit() {
// ✅ Add all subscriptions to container
this.subscriptions.add(
this.userService.getUsers().subscribe(users => (this.users = users))
);

this.subscriptions.add(
interval(1000).subscribe(() => console.log('Polling...'))
);
}

ngOnDestroy() {
// ✅ Cleanup all subscriptions
this.subscriptions.unsubscribe();
}
}

12. RxJS - Nested Subscriptions (Callback Hell)

Question: "Refactor this nested subscription hell."

❌ Bad Practice:

@Component({
selector: 'app-order-details',
standalone: true,
})
export class OrderDetailsComponent implements OnInit {
orderDetails: any;

ngOnInit() {
// ❌ PROBLEM: Nested subscriptions (pyramid of doom)
// ❌ PROBLEM: Multiple HTTP requests in sequence
// ❌ PROBLEM: No error handling
// ❌ PROBLEM: Hard to read and maintain
this.route.params.subscribe(params => {
const orderId = params['id'];

this.orderService.getOrder(orderId).subscribe(order => {
this.order = order;

this.userService.getUser(order.userId).subscribe(user => {
this.user = user;

this.productService.getProduct(order.productId).subscribe(product => {
this.product = product;

this.orderDetails = { order, user, product };
});
});
});
});
}
}

✅ Fix: Use Higher-Order Operators

@Component({
selector: 'app-order-details',
standalone: true,
imports: [AsyncPipe],
})
export class OrderDetailsComponent {
private route = inject(ActivatedRoute);
private orderService = inject(OrderService);
private userService = inject(UserService);
private productService = inject(ProductService);

// ✅ Clean, declarative data flow
orderDetails$ = this.route.params.pipe(
// ✅ Extract order ID
map(params => params['id']),

// ✅ Switch to order request (cancels previous if route changes)
switchMap(orderId => this.orderService.getOrder(orderId)),

// ✅ Load user and product in parallel
switchMap(order =>
forkJoin({
order: of(order),
user: this.userService.getUser(order.userId),
product: this.productService.getProduct(order.productId),
})
),

// ✅ Error handling
catchError(error => {
console.error('Failed to load order details:', error);
return of(null);
}),

// ✅ Share result to avoid multiple subscriptions
shareReplay(1)
);
}

Template:

@if (orderDetails$ | async; as details) {
<div>
<h2>Order #{{ details.order.id }}</h2>
<p>Customer: {{ details.user.name }}</p>
<p>Product: {{ details.product.name }}</p>
</div>
} @else {
<p>Loading...</p>
}

13. RxJS - Subject Misuse

Question: "Fix the Subject usage issues."

❌ Bad Practice:

@Injectable({ providedIn: 'root' })
export class SearchService {
// ❌ PROBLEM: Subject exposed publicly - can be abused
searchResults = new Subject<string[]>();

search(query: string) {
this.http.get<string[]>(`/api/search?q=${query}`).subscribe(results => {
this.searchResults.next(results);
});
}
}

// In component:
this.searchService.searchResults.next(['hacked!']); // ❌ Anyone can emit!

✅ Fix: Expose Only Observable

@Injectable({ providedIn: 'root' })
export class SearchService {
// ✅ Private subject
private searchResultsSubject = new BehaviorSubject<string[]>([]);

// ✅ Expose only observable (read-only)
readonly searchResults$ = this.searchResultsSubject.asObservable();

// ✅ Or use signal for better integration
private resultsSignal = signal<string[]>([]);
readonly results = this.resultsSignal.asReadonly();

search(query: string): Observable<string[]> {
return this.http.get<string[]>(`/api/search?q=${query}`).pipe(
tap(results => {
this.searchResultsSubject.next(results);
this.resultsSignal.set(results);
})
);
}
}

14. Change Detection - Unnecessary Checks

Question: "Optimize this component's change detection."

❌ Bad Practice:

@Component({
selector: 'app-product-card',
standalone: true,
template: `
<div>
<h3>{{ product.name }}</h3>
<!-- ❌ PROBLEM: Function called on every CD cycle! -->
<p>{{ calculateDiscount() }}</p>
<p>{{ formatDate(product.createdAt) }}</p>
<button (click)="addToCart()">Add to Cart</button>
</div>
`,
})
export class ProductCardComponent {
@Input() product!: Product;

// ❌ PROBLEM: Heavy calculation runs on EVERY change detection
calculateDiscount(): number {
console.log('Calculating discount...'); // This logs constantly!
let discount = 0;
// Complex calculation...
for (let i = 0; i < 1000; i++) {
discount += this.product.price * 0.001;
}
return discount;
}

formatDate(date: Date): string {
console.log('Formatting date...'); // This logs constantly!
return new Intl.DateTimeFormat('en-US').format(date);
}

addToCart() {
// Action
}
}

✅ Fix: OnPush + Computed Values

@Component({
selector: 'app-product-card',
standalone: true,
// ✅ Enable OnPush - only check when inputs change
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div>
<h3>{{ product.name }}</h3>
<!-- ✅ Use computed properties -->
<p>{{ discount }}</p>
<p>{{ formattedDate }}</p>
<button (click)="addToCart()">Add to Cart</button>
</div>
`,
})
export class ProductCardComponent implements OnChanges {
@Input() product!: Product;

// ✅ Cache calculated values
discount = 0;
formattedDate = '';

ngOnChanges(changes: SimpleChanges) {
// ✅ Only recalculate when product input changes
if (changes['product']) {
this.discount = this.calculateDiscount();
this.formattedDate = this.formatDate(this.product.createdAt);
}
}

private calculateDiscount(): number {
console.log('Calculating discount once!');
let discount = 0;
for (let i = 0; i < 1000; i++) {
discount += this.product.price * 0.001;
}
return discount;
}

private formatDate(date: Date): string {
console.log('Formatting date once!');
return new Intl.DateTimeFormat('en-US').format(date);
}

addToCart() {
// Action
}
}

✅ Better: Use Signals

@Component({
selector: 'app-product-card',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div>
<h3>{{ product().name }}</h3>
<!-- ✅ Computed values auto-update when product changes -->
<p>{{ discount() }}</p>
<p>{{ formattedDate() }}</p>
<button (click)="addToCart()">Add to Cart</button>
</div>
`,
})
export class ProductCardComponent {
// ✅ Signal input (Angular 17.1+)
product = input.required<Product>();

// ✅ Computed values - only recalculate when product changes
discount = computed(() => {
console.log('Calculating discount - memoized!');
let discount = 0;
for (let i = 0; i < 1000; i++) {
discount += this.product().price * 0.001;
}
return discount;
});

formattedDate = computed(() => {
console.log('Formatting date - memoized!');
return new Intl.DateTimeFormat('en-US').format(this.product().createdAt);
});

addToCart() {
// Action
}
}

15. NgRx - Improper State Mutations

Question: "Fix the state mutation issues in this reducer."

❌ Bad Practice:

import { createReducer, on } from '@ngrx/store';
import { addUser, updateUser, deleteUser } from './user.actions';

export interface UserState {
users: User[];
selectedId: string | null;
}

const initialState: UserState = {
users: [],
selectedId: null,
};

export const userReducer = createReducer(
initialState,

on(addUser, (state, { user }) => {
// ❌ PROBLEM: Direct state mutation!
state.users.push(user);
return state;
}),

on(updateUser, (state, { user }) => {
// ❌ PROBLEM: Mutating array element directly!
const index = state.users.findIndex(u => u.id === user.id);
state.users[index] = user;
return state;
}),

on(deleteUser, (state, { id }) => {
// ❌ PROBLEM: Mutating array with splice!
const index = state.users.findIndex(u => u.id === id);
state.users.splice(index, 1);
return state;
})
);

Why It's Bad:

  • NgRx relies on reference equality for change detection
  • Mutations break time-travel debugging
  • Can cause subtle bugs and unpredictable behavior
  • OnPush components won't detect changes

✅ Fix: Immutable Updates

export const userReducer = createReducer(
initialState,

on(addUser, (state, { user }) => ({
// ✅ Return new state object
...state,
// ✅ Create new array with spread operator
users: [...state.users, user],
})),

on(updateUser, (state, { user }) => ({
...state,
// ✅ Map to new array, update matching user
users: state.users.map(u => (u.id === user.id ? { ...u, ...user } : u)),
})),

on(deleteUser, (state, { id }) => ({
...state,
// ✅ Filter creates new array
users: state.users.filter(u => u.id !== id),
}))
);

✅ Better: Use NgRx Entity Adapter

import { createEntityAdapter, EntityState } from '@ngrx/entity';

export interface UserState extends EntityState<User> {
selectedId: string | null;
}

// ✅ Entity adapter handles immutability automatically
export const userAdapter = createEntityAdapter<User>();

const initialState: UserState = userAdapter.getInitialState({
selectedId: null,
});

export const userReducer = createReducer(
initialState,

// ✅ Built-in immutable operations
on(addUser, (state, { user }) => userAdapter.addOne(user, state)),

on(updateUser, (state, { user }) =>
userAdapter.updateOne({ id: user.id, changes: user }, state)
),

on(deleteUser, (state, { id }) => userAdapter.removeOne(id, state)),

on(loadUsersSuccess, (state, { users }) => userAdapter.setAll(users, state))
);

// ✅ Built-in selectors
export const { selectIds, selectEntities, selectAll, selectTotal } =
userAdapter.getSelectors();

16. NgRx - Effects Side Effect Issues

Question: "Identify and fix the issues in these effects."

❌ Bad Practice:

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { map } from 'rxjs/operators';

@Injectable()
export class UserEffects {
constructor(
private actions$: Actions,
private userService: UserService,
private store: Store
) {}

// ❌ PROBLEM: No error handling!
// ❌ PROBLEM: If request fails, effect dies and never works again
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType('[User] Load Users'),
switchMap(() => this.userService.getUsers()),
map(users => ({ type: '[User] Load Success', users }))
)
);

// ❌ PROBLEM: Effect doesn't dispatch an action
// ❌ PROBLEM: Side effect without proper error handling
saveUser$ = createEffect(() =>
this.actions$.pipe(
ofType('[User] Save User'),
switchMap(action => this.userService.saveUser(action.user)),
map(() => {
// ❌ PROBLEM: Direct console.log in effect
console.log('User saved!');
// ❌ PROBLEM: No action dispatched!
})
)
);

// ❌ PROBLEM: Infinite loop - dispatches action it listens to
logActions$ = createEffect(() =>
this.actions$.pipe(
ofType('[User] Log Action'),
map(() => {
console.log('Action logged');
// ❌ PROBLEM: Dispatches same action type = infinite loop!
return { type: '[User] Log Action' };
})
)
);
}

✅ Fix: Proper Error Handling and Action Flow

@Injectable()
export class UserEffects {
private actions$ = inject(Actions);
private userService = inject(UserService);
private store = inject(Store);

// ✅ Proper error handling with catchError
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.loadUsers),
switchMap(() =>
this.userService.getUsers().pipe(
// ✅ Map to success action
map(users => UserActions.loadUsersSuccess({ users })),
// ✅ Catch errors and return error action
catchError(error =>
of(
UserActions.loadUsersFailure({
error: error.message,
})
)
)
)
)
)
);

// ✅ Dispatch success/failure actions
saveUser$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.saveUser),
switchMap(({ user }) =>
this.userService.saveUser(user).pipe(
map(savedUser => UserActions.saveUserSuccess({ user: savedUser })),
catchError(error =>
of(
UserActions.saveUserFailure({
error: error.message,
})
)
)
)
)
)
);

// ✅ Non-dispatching effect for side effects only
saveUserSuccess$ = createEffect(
() =>
this.actions$.pipe(
ofType(UserActions.saveUserSuccess),
tap(() => {
// ✅ Side effects here (logging, notifications, etc.)
console.log('User saved successfully!');
this.notificationService.show('User saved!');
})
),
{ dispatch: false }
); // ✅ Explicitly mark as non-dispatching

// ✅ Load user details after selection
selectUser$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.selectUser),
// ✅ Get current state to check if we need to load
concatLatestFrom(() => this.store.select(selectUserEntities)),
filter(([{ userId }, entities]) => !entities[userId]),
switchMap(([{ userId }]) =>
this.userService.getUser(userId).pipe(
map(user => UserActions.loadUserSuccess({ user })),
catchError(error =>
of(
UserActions.loadUserFailure({
error: error.message,
})
)
)
)
)
)
);

// ✅ Retry failed requests
retryableLoad$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.loadUsers),
switchMap(() =>
this.userService.getUsers().pipe(
// ✅ Retry 3 times with exponential backoff
retry({
count: 3,
delay: (error, retryCount) => timer(Math.pow(2, retryCount) * 1000),
}),
map(users => UserActions.loadUsersSuccess({ users })),
catchError(error =>
of(
UserActions.loadUsersFailure({
error: error.message,
})
)
)
)
)
)
);
}

17. Data Flow - Race Conditions

Question: "Fix the race condition in this search implementation."

❌ Bad Practice:

@Component({
selector: 'app-search',
standalone: true,
template: `
<input (input)="onSearch($event)" placeholder="Search..." />
@for (result of results; track result.id) {
<div>{{ result.name }}</div>
}
`,
})
export class SearchComponent {
results: any[] = [];

onSearch(event: Event) {
const query = (event.target as HTMLInputElement).value;

// ❌ PROBLEM: Each keystroke triggers new request
// ❌ PROBLEM: Results can arrive out of order
// ❌ PROBLEM: No debouncing - hammers the server
this.searchService.search(query).subscribe(results => {
this.results = results;
});
}
}

// Example race condition:
// User types "angular"
// Request 1: "a" (slow - takes 500ms)
// Request 2: "an" (fast - takes 100ms)
// Request 3: "ang" (fast - takes 100ms)
// Request 4: "angu" (fast - takes 100ms)
// Request 5: "angul" (fast - takes 100ms)
// Request 6: "angula" (fast - takes 100ms)
// Request 7: "angular" (fast - takes 100ms)
//
// Results arrive: "an", "ang", "angu", "angul", "angula", "angular", "a"
// Final displayed results: "a" ❌ WRONG!

✅ Fix: Proper RxJS Operators

@Component({
selector: 'app-search',
standalone: true,
imports: [AsyncPipe, FormsModule],
template: `
<input [(ngModel)]="searchQuery" placeholder="Search..." />

@if (loading()) {
<div>Loading...</div>
} @for (result of results$ | async; track result.id) {
<div>{{ result.name }}</div>
}
`,
})
export class SearchComponent {
private searchService = inject(SearchService);

// ✅ Use signal for input
searchQuery = signal('');
loading = signal(false);

// ✅ Convert signal to observable for RxJS operators
private searchQuery$ = toObservable(this.searchQuery);

// ✅ Proper search pipeline
results$ = this.searchQuery$.pipe(
// ✅ Debounce to avoid hammering server
debounceTime(300),

// ✅ Only search if query is not empty
filter(query => query.length > 0),

// ✅ Avoid duplicate searches
distinctUntilChanged(),

// ✅ Show loading state
tap(() => this.loading.set(true)),

// ✅ Cancel previous requests, use latest query only
switchMap(query =>
this.searchService.search(query).pipe(
// ✅ Handle errors gracefully
catchError(() => of([]))
)
),

// ✅ Hide loading state
tap(() => this.loading.set(false)),

// ✅ Start with empty array
startWith([] as SearchResult[])
);
}

✅ Alternative: Signals-Only Approach (Angular 17+)

@Component({
selector: 'app-search',
standalone: true,
template: `
<input
[value]="searchQuery()"
(input)="searchQuery.set($any($event.target).value)"
placeholder="Search..."
/>

@if (loading()) {
<div>Loading...</div>
} @for (result of results(); track result.id) {
<div>{{ result.name }}</div>
}
`,
})
export class SearchComponent {
private searchService = inject(SearchService);
private destroyRef = inject(DestroyRef);

searchQuery = signal('');
loading = signal(false);
results = signal<SearchResult[]>([]);

constructor() {
// ✅ Use effect with proper RxJS handling
effect(() => {
const query = this.searchQuery();

if (query.length === 0) {
this.results.set([]);
return;
}

// Convert to observable for debounce and switchMap
of(query)
.pipe(
debounceTime(300),
distinctUntilChanged(),
tap(() => this.loading.set(true)),
switchMap(q => this.searchService.search(q)),
takeUntilDestroyed(this.destroyRef)
)
.subscribe({
next: results => {
this.results.set(results);
this.loading.set(false);
},
error: () => {
this.results.set([]);
this.loading.set(false);
},
});
});
}
}

18. Component Communication - Anti-Patterns

Question: "Refactor this tightly coupled component communication."

❌ Bad Practice:

// ❌ PROBLEM: Direct component references and tight coupling
@Component({
selector: 'app-parent',
template: `
<app-header #header></app-header>
<app-sidebar #sidebar></app-sidebar>
<app-content #content></app-content>
`,
})
export class ParentComponent {
@ViewChild('header') header!: HeaderComponent;
@ViewChild('sidebar') sidebar!: SidebarComponent;
@ViewChild('content') content!: ContentComponent;

// ❌ PROBLEM: Parent manipulating child state directly
updateTheme(theme: string) {
this.header.theme = theme;
this.sidebar.theme = theme;
this.content.theme = theme;
}

// ❌ PROBLEM: Tight coupling to child methods
onUserLogin(user: User) {
this.header.setUser(user);
this.sidebar.loadUserPreferences(user.id);
this.content.refreshForUser(user);
}
}

✅ Fix: Service-Based Communication

// ✅ Shared service for loose coupling
@Injectable({ providedIn: 'root' })
export class ThemeService {
private themeSignal = signal<'light' | 'dark'>('light');
readonly theme = this.themeSignal.asReadonly();

setTheme(theme: 'light' | 'dark') {
this.themeSignal.set(theme);
}
}

@Injectable({ providedIn: 'root' })
export class AuthService {
private currentUserSignal = signal<User | null>(null);
readonly currentUser = this.currentUserSignal.asReadonly();

login(user: User) {
this.currentUserSignal.set(user);
}
}

// ✅ Components subscribe to services
@Component({
selector: 'app-header',
template: `
<header [class]="theme()">
@if (user(); as user) {
<span>Welcome, {{ user.name }}</span>
}
</header>
`,
})
export class HeaderComponent {
private themeService = inject(ThemeService);
private authService = inject(AuthService);

theme = this.themeService.theme;
user = this.authService.currentUser;
}

@Component({
selector: 'app-parent',
template: `
<app-header></app-header>
<app-sidebar></app-sidebar>
<app-content></app-content>
<button (click)="toggleTheme()">Toggle Theme</button>
`,
})
export class ParentComponent {
private themeService = inject(ThemeService);

// ✅ Parent just updates service, children react automatically
toggleTheme() {
const current = this.themeService.theme();
this.themeService.setTheme(current === 'light' ? 'dark' : 'light');
}
}

Key Takeaways

Memory Leaks

  • ✅ Always use AsyncPipe or takeUntilDestroyed()
  • ✅ Unsubscribe in ngOnDestroy
  • ❌ Never leave subscriptions open

RxJS Best Practices

  • ✅ Use switchMap for latest value (cancels previous)
  • ✅ Use mergeMap for parallel requests
  • ✅ Use concatMap for sequential order
  • ✅ Always handle errors with catchError
  • ❌ Avoid nested subscriptions

NgRx Principles

  • ✅ Never mutate state directly
  • ✅ Use Entity Adapter for collections
  • ✅ Handle errors in effects
  • ✅ Mark non-dispatching effects with { dispatch: false }
  • ❌ Don't create infinite loops

Performance

  • ✅ Use OnPush change detection
  • ✅ Use computed() for derived values
  • ✅ Memoize expensive calculations
  • ❌ Avoid function calls in templates
  • ❌ Avoid direct DOM manipulation

Component Communication

  • ✅ Use services for shared state
  • ✅ Use signals for reactive updates
  • ✅ Prefer loose coupling
  • ❌ Avoid direct component references
  • ❌ Avoid tight coupling

Practice Tips

  1. Draw diagrams from memory - Don't memorize, understand
  2. Explain trade-offs - Every solution has pros/cons
  3. Use real examples - Reference actual code you've written
  4. Think aloud - Show your problem-solving process
  5. Ask clarifying questions - Demonstrate thoroughness
  6. Spot anti-patterns - Recognize bad code quickly
  7. Explain fixes - Don't just fix, explain why

Time Guidelines

  • Simple concept: 2-3 minutes
  • Architecture diagram: 5-7 minutes
  • Complex system design: 10-15 minutes
  • Code review: 5-10 minutes per example

Practice these until you can explain each concept clearly and confidently!