Angular

Signals

15 Questions

Signals (Angular 16+) are reactive primitives for state management. They track when values change and automatically update dependent computations and views. Signals always have a current value and are synchronous. Use set() to replace a value and update() to derive a new value from the current one.
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.

computed() creates a derived signal that automatically recalculates when its dependencies change. It tracks which signals are read during execution and re-runs when any of them update. Computed signals are lazy and only recalculate when read. They are read-only and cannot be set directly.
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.

effect() runs side effects when signals it reads change. It automatically tracks signal dependencies and re-runs whenever any of them update. Effects run at least once when created and then re-run on every dependency change. They are useful for logging, syncing to localStorage, or triggering external API calls.
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.

Signals are synchronous and always have a current value. Observables can be async and may not have a value until subscribed. Signals auto-track dependencies while Observables require explicit subscription. No subscribe/unsubscribe needed with signals. RxJS is better for event streams, HTTP, and complex async flows.
// 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.

toSignal() converts an Observable to a Signal for use in templates without the async pipe. It subscribes to the Observable and updates the signal with each emitted value. Provide an initialValue since signals need a synchronous value immediately. It automatically unsubscribes when the component is destroyed.
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.

toObservable() converts a Signal to an Observable, enabling use of RxJS operators like debounceTime, switchMap, and filter. This is useful when you need async processing on signal changes. The Observable emits whenever the signal value changes. It bridges signals with existing RxJS-based code.
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.

Angular 17.1+ introduces signal-based inputs using the input() function. They replace the @Input() decorator with a more type-safe and reactive approach. Inputs can be optional, required, or have default values. They integrate directly with computed() for derived state based on input values.
@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.

model() creates a two-way bindable signal (Angular 17.2+). It replaces the @Input/@Output pattern for two-way binding with a single signal-based API. The parent can bind using [(value)] syntax and both parent and child see the same reactive value. model() is a writable signal that emits changes to the parent.
@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.

Call signals like functions in templates using parentheses. Angular automatically tracks which signals are used in the template and updates only the relevant DOM bindings when they change. This is more efficient than zone-based change detection. No async pipe is needed for signals.
@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.

update() modifies a signal based on its current value by passing a callback function. Unlike set() which replaces the value entirely, update() receives the current value and returns the new value. This is essential when the new value depends on the current state. Both methods trigger reactivity and notify dependents.
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.

set() replaces the signal's value entirely with a new value. update() takes a callback function that receives the current value and returns the new value. Use set() when you know the exact new value. Use update() when the new value depends on the current value like incrementing or appending.
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.

No, signals and RxJS serve different purposes and work best together. Signals are designed for synchronous state management and always have a current value. RxJS is designed for asynchronous event streams like HTTP requests, WebSocket messages, and complex async workflows. Angular provides toSignal() and toObservable() to bridge between them.
// 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.

The output() function (Angular 17.2+) is the signal-based replacement for @Output with EventEmitter. It creates a typed output that components can bind to in templates. The emit method sends values to the parent. Unlike EventEmitter, output() does not rely on RxJS Observables and is a lightweight alternative.
@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.

linkedSignal (Angular 18+) creates a writable signal whose value is automatically reset when a source signal changes. It acts like a computed signal that you can also manually set. This bridges the gap between computed (read-only) and regular signals (no auto-tracking). It is useful for values that should reset when their parent changes.
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.

Signals integrate with Angular's change detection by marking the component for check when a signal used in the template changes. With zoneless mode, signals enable fine-grained reactivity where only specific DOM bindings tied to the changed signal are updated. In OnPush components, signals automatically trigger the right updates without needing markForCheck().
@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.