In Angular, why does `toObservable(symbol)` emit only the last of three synchronous `symbol.set()` calls, and when does it emit at all?
answer
- an effect watches the signal
- effects run later, not per write
- a one-value replay buffer
- first value may arrive on subscribe
- completes with its injector
basics
~10 sAngular's toObservable() forwards a signal's value through an effect into a ReplaySubject(1); the effect runs later and reads only the current value, so several synchronous set() calls produce one emission of the last value.
solid answer
~40 s`toObservable()` creates an `effect()` that reads the signal and pushes its value into a `ReplaySubject(1)`, and returns that subject as an Observable. Signals do not notify synchronously per write; the effect is scheduled and runs later, reading whatever the value is by then. So `set(1); set(2); set(3)` in one task produce a single emission of `3`, and intermediate values are never observed. Because of the replay buffer, a subscriber that arrives after the effect has run gets the current value synchronously on subscribe; later changes arrive asynchronously. The call needs an injection context (or an `injector` option), and the Observable completes when that injector's `DestroyRef` fires. If the signal throws when read, the Observable errors.
code
ts · 22 linesimport { Component, inject, signal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { switchMap } from 'rxjs';
import { PriceFeed } from './price-feed';
@Component({
selector: 'app-symbol-switcher',
template: `
@for (s of symbols; track s) { <button (click)="symbol.set(s)">{{ s }}</button> }
<p>{{ quote()?.price ?? '...' }}</p>
`,
})
export class SymbolSwitcher {
readonly symbols = ['AAPL', 'MSFT', 'NVDA'];
readonly symbol = signal('AAPL');
private readonly feed = inject(PriceFeed);
// One effect feeds symbol changes into the stream; switchMap moves to the latest feed.
readonly quote = toSignal(
toObservable(this.symbol).pipe(switchMap(s => this.feed.watch(s))),
);
}go deeper
Know that toObservable turns a signal into an Observable, so RxJS operators can react to its changes, and that it is called once in an injection context.
Explain the effect plus ReplaySubject(1) design, why synchronous writes coalesce, and when a subscriber receives the first value.
Pick toObservable only for state, not for events, and reason about its asynchronous delivery when combining it with switchMap-based feeds.
Decide where the signal-to-stream boundary sits in the architecture so event streams and state signals are not converted back and forth needlessly.
## How toObservable is built `toObservable(source)` from `@angular/core/rxjs-interop` (stable since v20) has a small implementation worth knowing: 1. It creates a **`ReplaySubject` with a buffer of one**. 2. It creates an **`effect()`** that reads `source()` and calls `subject.next(value)`; if reading the signal throws, it calls `subject.error(err)` instead. 3. It registers with the injector's **`DestroyRef`** to destroy the effect and **complete** the subject. 4. It returns `subject.asObservable()`. Everything about its timing follows from those four lines. ## Why only the last value arrives Signals are **not event streams**. Writing to a signal marks its consumers as dirty; it does not call them. The effect that `toObservable` created runs **later**, when Angular processes scheduled effects, and it reads the signal's value **at that moment**. ```ts const symbol$ = toObservable(this.symbol); symbol$.subscribe(s => console.log(s)); this.symbol.set('AAPL'); this.symbol.set('MSFT'); this.symbol.set('NVDA'); // only 'NVDA' is logged, once the effect runs ``` Angular's guide shows the same thing with numbers: after three synchronous `set` calls only the last value is logged. The exact scheduling of effects belongs to the effects topic; the key point here is that **one effect run sees one value**. Two more consequences: - Setting a signal to a value **equal** to its current one (by the signal's equality function) does not mark consumers dirty, so nothing is emitted. - `toObservable` is **the wrong tool to count events**. If every click or keystroke must be seen, use a `Subject` or an `output()` stream, not a signal. ## When it emits on subscribe | Situation | What a new subscriber receives | |---|---| | effect has not run yet (just created) | nothing until the effect runs | | effect has run at least once | the current value, **synchronously** on subscribe, from the replay buffer | | later signal changes | the new value, **asynchronously**, once per effect run | | owner destroyed | completion | All subscribers share **one effect and one buffer**; subscribing again does not create another effect. ## Injection context and lifetime The effect needs an injector, so `toObservable()` must run in an **injection context** or receive `{ injector }`. The injector's `DestroyRef` decides the lifetime: in a component the Observable completes when the component is destroyed, which also completes any `switchMap` pipeline built on it once its inner work finishes or is unsubscribed. ## A typical use The price-feed screen turns the selected symbol into a stream so an RxJS operator can switch feeds: 1. `toObservable(this.symbol)` gives the symbol as a stream. 2. `switchMap(s => this.feed.watch(s))` subscribes to the new symbol's feed and drops the old one. 3. Several writes to the symbol in the same task (for example a reset followed by a restore) collapse into one effect run, so the feed switches once, to the final symbol. ## What interviewers listen for - An **effect** plus a **`ReplaySubject(1)`**, not a synchronous bridge. - **Synchronous writes coalesce**; only the value current at the effect run is emitted. - The **first value can be synchronous** for late subscribers; later ones are asynchronous. - It needs an **injection context** and **completes** with its injector.
- Does subscribing twice to the same toObservable result create two effects?No. The effect is created once, when `toObservable()` is called, and it feeds a single `ReplaySubject(1)`. Every subscriber shares that subject; a late one receives the buffered current value and then the same later emissions.
- What happens to the Observable if the signal it watches throws when read?The effect catches the error and calls `error` on the subject, so subscribers receive an error notification and the stream is finished. A computed signal that throws is the usual way this happens.
saying these in an interview costs you the question
- toObservable emits synchronously on every signal write
- Each subscriber to a toObservable result gets its own effect
- toObservable replays every value the signal has ever held
- toObservable never completes, even after its component is destroyed
- toObservable is a good way to count button clicks stored in a signal