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 Case | Solution | Why |
|---|---|---|
| Component state | Signals | Synchronous, simple, local |
| HTTP requests | RxJS | Async, cancellation, retry |
| Form validation | Signals | Immediate feedback |
| WebSocket streams | RxJS | Continuous async data |
| Computed values | Signals | Auto-tracking, memoized |
| Complex operators | RxJS | debounce, 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
@deferenables 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:
bootstrapApplicationproviders - 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:
-
OnPush Change Detection
@Component({ changeDetection: ChangeDetectionStrategy.OnPush }) -
Lazy Loading Routes
loadComponent: () => import('./feature.component'); -
NgOptimizedImage
<img ngSrc="hero.jpg" width="1200" height="600" priority /> -
@defer for Heavy Components
@defer (on viewport) { <chart /> } -
TrackBy in Lists
@for (item of items; track item.id) { } -
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
AsyncPipeortakeUntilDestroyed() - ✅ Unsubscribe in
ngOnDestroy - ❌ Never leave subscriptions open
RxJS Best Practices
- ✅ Use
switchMapfor latest value (cancels previous) - ✅ Use
mergeMapfor parallel requests - ✅ Use
concatMapfor 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
OnPushchange 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
- Draw diagrams from memory - Don't memorize, understand
- Explain trade-offs - Every solution has pros/cons
- Use real examples - Reference actual code you've written
- Think aloud - Show your problem-solving process
- Ask clarifying questions - Demonstrate thoroughness
- Spot anti-patterns - Recognize bad code quickly
- 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!