In an Angular component, how does `toSignal()` expose a WebSocket price feed to the template, and when does it subscribe and unsubscribe?
answer
- call once, read many times
- subscribes at the call, not the read
- latest emission becomes the value
- DestroyRef of the calling context
- errors rethrown on read
basics
~10 sAngular's toSignal() subscribes to the Observable as soon as it is called, stores each emission in a read-only signal the template reads with (), and unsubscribes when the calling component's or service's DestroyRef fires.
solid answer
~40 s`toSignal()` from `@angular/core/rxjs-interop` (stable since v20) is called once, typically in a field initializer: `quote = toSignal(this.feed.quotes$)`. It **subscribes immediately**, at the call, not when the signal is first read, so the feed starts even if the template has not rendered that part yet. Each `next` writes the value into an internal signal; the returned read-only signal is what the template reads as `quote()`, and signal reads let Angular refresh the view under `OnPush` and zoneless defaults. The subscription is tied to the **`DestroyRef` of the injection context** that called it, so it ends when the component (or service) is destroyed. If the Observable errors, reading the signal throws that error; if it completes, the signal keeps the last value.
code
ts · 11 linesimport { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { webSocket } from 'rxjs/webSocket';
export interface Quote { symbol: string; price: number; }
@Injectable({ providedIn: 'root' })
export class PriceFeed {
private readonly socket = webSocket<Quote>('wss://example.test/quotes');
readonly quotes$: Observable<Quote> = this.socket.asObservable();
}go deeper
Know that toSignal turns an Observable into a signal you read with (), is called once in a field initializer, and cleans up when the component is destroyed.
Explain the eager subscription, undefined before the first value, errors thrown on read and the last value kept after completion.
Decide where conversion should happen so eager subscriptions do not start expensive feeds early, and handle stream errors before they reach the signal.
Set the boundary between Observable-based services and signal-based components across the codebase, including who owns connection lifetimes for shared feeds.
## What toSignal gives you Angular templates and `computed()` work with **signals**; much existing code (WebSocket feeds, router streams, services built on subjects) produces **Observables**. `toSignal()` from `@angular/core/rxjs-interop` bridges the two: it subscribes to an Observable and returns a **read-only `Signal`** whose value is always the most recent emission. It is stable since v20. ```ts export class PriceTicker { private readonly feed = inject(PriceFeed); readonly quote = toSignal(this.feed.quotes$); } ``` The template reads `quote()` like any other signal, and a `computed()` can derive from it. ## When it subscribes `toSignal()` subscribes **at the moment it is called**, not lazily on first read. In the example the subscription starts while the component is being constructed. - For a **WebSocket** source, that means the socket is opened (or joined) during construction, even if the part of the template that reads `quote()` sits inside a block that has not rendered yet. - For a **cold** source such as an `HttpClient` call, the request goes out at construction. - Every call creates **its own subscription**. Angular's guide says to avoid calling it repeatedly for the same Observable and to reuse the signal it returns. ## What the signal holds over time | Moment | `quote()` returns | |---|---| | before the first emission | `undefined` (unless an initial value is configured) | | after each `next` | the latest emitted value | | after the Observable completes | the last value, unchanged | | after the Observable errors | reading **throws** that error | The signal notifies its consumers only when the new value is different by its equality function (`Object.is` by default, or a custom `equal` option), so emitting the same primitive twice does not trigger work. ## When it unsubscribes By default `toSignal()` looks up a **`DestroyRef`** and registers the unsubscribe with it: - called in a **component or directive**, the subscription ends when that component or directive is destroyed; - called in a **service**, it ends when the injector that owns the service is destroyed, which for `providedIn: 'root'` means the application. Because it needs that `DestroyRef`, the plain call must run in an **injection context**: a constructor or field initializer. Other options for calling it elsewhere are a separate question. ## Why it suits a signal-based template 1. The template reads a value, not a stream, so there is no `| async` and no `null`-handling from the pipe. 2. Reading a signal in a template registers the view as a consumer, so each new quote marks the view for refresh without manual `markForCheck()`, which matters with `OnPush` as the v22 default and zoneless as the v21 default. 3. The value can feed `computed()` directly, for example a formatted price or a colour for up and down moves. ## Pitfalls - **Eager side effects**: a feed that should start only when a panel opens should not be converted in the component's constructor. - **One call per stream**: calling `toSignal()` inside a method, getter or template expression creates new subscriptions. - **Errors surface on read**: an unhandled error makes every read throw, which can break the whole template; handle errors in the stream first with `catchError` when a fallback is wanted. ## What interviewers listen for - It **subscribes immediately** when called, not on read. - The value is `undefined` until the first emission. - It **unsubscribes via `DestroyRef`** of the calling context. - Errors are **rethrown on read**; completion keeps the last value.
- Does toSignal subscribe again each time the template reads the signal?No. It subscribes once, when `toSignal()` is called, and every read returns the value stored by that single subscription. Reads are cheap; calls are what create subscriptions, which is why the call belongs in a field initializer or constructor.
- What does a toSignal signal return after the source Observable completes?It keeps returning the most recent value emitted before completion. Signals have no completed state, so completion simply means no further updates, and the subscription's destroy callback is unregistered.
- Why can an unhandled error in the source break a whole template?After an error, every read of the signal throws that error. A template or computed that reads it fails on each evaluation, so handle errors in the stream with `catchError` and map them to a value the template can render.
It is like a live stock ticker on an office wall: someone connects it when the office opens (the call), anyone glancing at it sees the latest price (the read), and it is unplugged when the office closes (DestroyRef).
saying these in an interview costs you the question
- toSignal subscribes lazily the first time the signal is read
- toSignal returns a writable signal you can set from the component
- A toSignal subscription must be unsubscribed manually in ngOnDestroy
- After the source completes, the signal resets to undefined
- Reading a toSignal signal after an error returns undefined