import { signal, computed, effect } from '@angular/core';
const count = signal(0);
count.set(5);
count.update(v => v + 1);
console.log(count()); // Read value: 6
Signals integrate directly with Angular's change detection. Reading a signal in a template creates an automatic dependency that updates the view when the signal changes.
Why it matters: Tests understanding of modern reactive state management. Shows you know how signals improve change detection.
Real applications: Component state, user input tracking, form values, counters, real-time data, computed values.
Common mistakes: Developers treat signals like regular variables forgetting they're reactive. They use set() for every change losing tracking. They don't understand signals update views automatically.
const firstName = signal('John');
const lastName = signal('Doe');
const fullName = computed(() => firstName() + ' ' + lastName());
console.log(fullName()); // "John Doe"
firstName.set('Jane');
console.log(fullName()); // "Jane Doe" - auto-updated
Computed signals are memoized so the same value is returned without recalculation until a dependency changes. They are ideal for derived state.
Why it matters: Tests understanding of derived state and optimization. Shows you know how to compute values efficiently.
Real applications: Full names from first/last names, totals from items, filtered lists, formatted dates, combining multiple signals.
Common mistakes: Developers don't use computed() creating duplicate code. They manually track dependencies. They can't modify computed() values forgetting they're read-only.
const user = signal({ name: 'John', age: 30 });
effect(() => {
console.log('User changed:', user().name);
// Runs when user signal changes
});
Effects must be created in an injection context (constructor or with inject). Use untracked() to read signals inside an effect without creating a dependency.
Why it matters: Tests understanding of side effects and reactive patterns. Shows you can perform actions when state changes.
Real applications: Logging state changes, syncing to localStorage, triggering API calls, analytics tracking, external system updates.
Common mistakes: Developers can't use effects outside components forgetting injection context requirement. They don't use untracked() creating infinite loops. They use effects instead of computed() for derived values.
// Signal: synchronous, always has value
const count = signal(0);
console.log(count()); // 0 immediately
// Observable: async, requires subscribe
const count$ = new BehaviorSubject(0);
count$.subscribe(val => console.log(val));
// Bridge between them
const signalFromObs = toSignal(count$);
const obsFromSignal = toObservable(count);
Use signals for synchronous state and RxJS for async operations. They complement each other through toSignal() and toObservable().
Why it matters: Tests understanding of when to use each paradigm. Shows you know integration between reactive approaches.
Real applications: Signals for local component state, RxJS for HTTP/WebSocket/async events, using both together for hybrid solutions.
Common mistakes: Developers use Observables for simple state (over-engineered). They use signals for async operations (missing operators). They don't understand each has different strengths.
import { toSignal } from '@angular/core/rxjs-interop';
const users = toSignal(this.http.get<User[]>('/api/users'), {
initialValue: []
});
// In template — no async pipe needed
<li *ngFor="let user of users()">{{ user.name }}</li>
toSignal must be called in an injection context. Without initialValue, the signal type includes undefined until the Observable emits.
Why it matters: Tests understanding of Observable to Signal conversion. Shows you can use async data with reactive templates.
Real applications: Converting HTTP Observables to signals, displaying async data without async pipe, using Observable results with computed signals.
Common mistakes: Developers forget initialValue getting undefined type issues. They use toSignal outside injection context (fails). They don't unsubscribe forgetting Angular handles it.
import { toObservable } from '@angular/core/rxjs-interop';
const search = signal('');
const search$ = toObservable(this.search);
search$.pipe(
debounceTime(300),
switchMap(q => this.api.search(q))
).subscribe(results => this.results.set(results));
Use toObservable when you need RxJS operators. Keep state in signals and use Observables only for async processing pipelines.
Why it matters: Tests understanding of Signal to Observable conversion for async processing.
Real applications: Search debouncing, auto-save with switchMap, validation checking, cascading selections, real-time filtering.
Common mistakes: Developers don't use toObservable() for complex async needs. They overuse effects when RxJS operators would work better. They subscribe without storing signal state.
@Component({...})
export class UserComponent {
name = input<string>(); // Optional input signal
id = input.required<number>(); // Required input signal
label = input('default'); // Input with default value
// Computed based on input
greeting = computed(() => 'Hello ' + this.name());
}
Signal inputs are read-only from within the component. They create a reactive dependency that updates computed signals and effects automatically.
Why it matters: Tests understanding of signal-based component APIs. Shows you know modern Angular input patterns.
Real applications: Dashboard widgets, reusable components, list items, forms with signal-based configuration.
Common mistakes: Developers try to modify input signals (they're read-only). They forget to use input() from Angular core. They don't leverage computed() based on inputs.
@Component({
selector: 'app-counter',
template: '<button (click)="increment()">{{ value() }}</button>'
})
export class CounterComponent {
value = model(0); // Two-way binding
increment() { this.value.update(v => v + 1); }
}
// Parent: <app-counter [(value)]="count" />
model() replaces the traditional @Input/@Output EventEmitter pattern. Changes propagate in both directions automatically.
Why it matters: Tests understanding of signal-based two-way binding. Shows you know the modern approach to parent-child communication.
Real applications: Modal dialogs with state, accordion components, expandable sections, toggle buttons, form inputs with two-way binding.
Common mistakes: Developers don't use model() still using @Input/@Output pattern. They forget model() is writable both ways. They don't understand automatic change propagation.
@Component({
template: '<p>Count: {{ count() }}</p><button (click)="increment()">+1</button>'
})
export class CounterComponent {
count = signal(0);
increment() { this.count.update(v => v + 1); }
}
Signals in templates create fine-grained reactive bindings. Only the specific DOM node tied to the changed signal is updated.
Why it matters: Tests understanding of signal usage in templates. Shows you know reactive template syntax.
Real applications: Live counters, state displays, conditional rendering, dynamic lists, event handling tied to signals.
Common mistakes: Developers forget parentheses thinking signals are variables. They don't understand call syntax creates dependencies. They use async pipe instead of signal() calls.
const items = signal<string[]>([]);
// set replaces entirely
items.set(['a', 'b']);
// update derives from current value
items.update(list => [...list, 'c']); // ['a', 'b', 'c']
Use update() for counters, arrays, and objects where the new value depends on the old one. Use set() when you have the complete new value.
Why it matters: Tests understanding of signal mutation patterns. Shows you know how to modify state correctly.
Real applications: Incrementing counters, adding items to lists, merging objects, form value updates, state transitions.
Common mistakes: Developers use set() for every change losing context of current value. They mutate arrays/objects in-place breaking reactivity. They don't leverage update() callback simplicity.
const count = signal(0);
// set: replace with new value directly
count.set(10);
// update: derive new value from current
count.update(current => current + 1); // 11
const items = signal<string[]>([]);
items.update(list => [...list, 'new item']);
Both methods trigger reactivity and update all dependent computed signals and effects. Choose based on whether you need the current value.
Why it matters: Tests understanding of signal mutation semantics. Shows you know fundamental operation differences.
Real applications: Form handling, data transformation, event processing, state updates, counter operations.
Common mistakes: Developers confuse set() and update() using wrong one. They don't understand both trigger reactivity. They forget update() takes a callback function.
// Signals: synchronous state
const count = signal(0);
const doubled = computed(() => count() * 2);
// RxJS: async operations (still needed)
this.http.get('/api/data').pipe(
retry(3),
catchError(err => of([]))
).subscribe(data => this.data.set(data));
// Bridge: convert Observable to Signal
const users = toSignal(this.userService.getUsers());
Signals replace simple BehaviorSubject patterns for state. RxJS is still essential for async operations and complex operator chains.
Why it matters: Tests understanding of signal limitations. Shows you know when to keep using RxJS.
Real applications: Choose signals for local state, RxJS for HTTP/WebSockets/timers, hybrid when you need both patterns.
Common mistakes: Developers think signals replace all RxJS eliminating powerful operators. They use RxJS for simple state over-complicating code. They don't understand complementary relationship.
@Component({
selector: 'app-search',
template: '<input (input)="onSearch($event)">'
})
export class SearchComponent {
searchChange = output<string>();
onSearch(event: Event) {
const value = (event.target as HTMLInputElement).value;
this.searchChange.emit(value);
}
}
// Parent: <app-search (searchChange)="handleSearch($event)" />
Use output() with signal-based inputs to create a fully signal-based component API. It provides better type inference than EventEmitter.
Why it matters: Tests understanding of signal-based component communication. Shows you know modern output patterns.
Real applications: Custom form controls, search components, filtering controls, modal dialogs, reusable widget communication.
Common mistakes: Developers don't use output() still using EventEmitter. They forget to call emit() on outputs. They don't understand typed outputs provide better type safety.
const users = signal<User[]>([]);
// selectedUser resets whenever users list changes
const selectedUser = linkedSignal(() => users()[0]);
// Can still manually set it
selectedUser.set(users()[2]);
// When users changes, selectedUser auto-resets to first user
Use linkedSignal for selected items that should reset when lists change, or form values that should reset when the parent entity changes.
Why it matters: Tests understanding of advanced signal patterns. Shows you know niche use cases for specialized signals.
Real applications: Selected user resets when user list changes, selected tab in panels, form values reset when parent entity loads, filtering state.
Common mistakes: Developers don't know linkedSignal exists. They use effects to manually reset on source changes. They don't understand auto-reset behavior.
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<p>{{ name() }}</p><button (click)="update()">Update</button>'
})
export class NameComponent {
name = signal('Angular');
update() { this.name.set('Updated'); } // auto-triggers view update
}
This is more efficient than zone-based change detection which checks the entire component tree. Signals enable the future zoneless Angular architecture.
Why it matters: Tests understanding of change detection optimization. Shows you know how signals improve performance.
Real applications: Large applications with many components, performance-critical dashboards, real-time data applications, games.
Common mistakes: Developers don't understand signals eliminate zone overhead. They use OnPush strategy + signals (good practice but signals alone sufficient). They don't leverage zoneless future.