In RxJS, what does fromEvent(button, 'click') do when you subscribe and unsubscribe, and when does the resulting stream complete?
answer
- a listener per subscription
- the teardown removes it
- events have no natural end
- options object and array-like targets
basics
~20 sfromEvent adds a click listener to the button on each subscribe, emits every event object, and removes that listener on unsubscribe. It never completes on its own, so something must unsubscribe or a take-style operator must end it.
solid answer
~40 s`fromEvent(button, 'click')` is lazy: creating it attaches nothing. Each `subscribe()` calls `button.addEventListener('click', handler)` with a handler that forwards the event object to `next`, and the subscription's teardown calls `removeEventListener` with the same handler. So two subscribers mean two listeners, and unsubscribing removes exactly one. The stream **never completes** by itself - a DOM element has no "no more clicks" signal - so ending it is the consumer's job: unsubscribe, or add an operator such as `take(1)`. A third argument passes listener options (`capture`, `passive`, `once`). It also accepts Node-style and jQuery-style emitters, and an array-like target such as a `NodeList` gets one listener per element. An unsupported target throws `TypeError: Invalid event target` when `fromEvent` is called.
code
ts · 16 linesimport { fromEvent, take } from 'rxjs';
const button = document.querySelector<HTMLButtonElement>('#refresh')!;
const refreshClicks$ = fromEvent<MouseEvent>(button, 'click');
// nothing is attached yet
const sub = refreshClicks$.subscribe((e) => console.log('clicked at', e.clientX, e.clientY));
// one listener attached
refreshClicks$.pipe(take(1)).subscribe({
next: () => console.log('first click'),
complete: () => console.log('done'), // take(1) completes; fromEvent alone never would
});
// later
sub.unsubscribe(); // removes only the first subscription's listenergo deeper
Recall that fromEvent adds a listener on subscribe, removes it on unsubscribe, and never completes by itself.
Explain one listener per subscription, why completion-dependent operators hang on event streams, and how take(1) differs from the once listener option.
Trace leaks and duplicate handlers back to subscriptions that outlive a screen or to several unshared subscriptions on scroll or mousemove sources.
Decide when event streams belong in RxJS at all versus plain template bindings, keeping streams for sources that genuinely need composition.
## What fromEvent builds RxJS's `fromEvent(target, eventName, options?)` turns an event source into an `Observable`. For a DOM element it wraps the platform's `addEventListener` and `removeEventListener` pair: - on **subscribe**, it creates a handler that calls `subscriber.next(event)` and registers it with `target.addEventListener(eventName, handler, options)`; - on **unsubscribe**, the teardown calls `target.removeEventListener(eventName, handler, options)` with that same handler. Calling `fromEvent(...)` only validates the target and picks its add and remove methods; the listener exists only while a subscription does. ## One listener per subscriber Because the handler is created inside the subscribe logic, **every subscription registers its own listener**. Two components subscribing to the same `clicks$` result in two listeners on the button, and one click produces two separate event deliveries. Unsubscribing one of them removes only its own listener. This is usually harmless for clicks; for expensive events such as `scroll` or `mousemove` it is worth sharing a single subscription. ## It never completes A DOM event source has no terminal signal - the browser never announces that a button will not be clicked again - so `fromEvent` **never calls `complete()`**. Consequences: - operators that wait for completion (`toArray`, `reduce`, `last`, `lastValueFrom`) never produce a result on a raw event stream; - the listener lives until someone unsubscribes, which is the source of classic leaks when a subscription outlives the element's screen; - to take a fixed number of events, add `take(n)`; to stop on a signal, add `takeUntil(...)`. The `{ once: true }` listener option is a trap here: the **browser** removes the listener after the first event, but RxJS does not know that, so the Observable emits once and then sits open forever without completing. `take(1)` is the correct way to get "first click, then done". ## Supported targets and options | Target kind | Methods RxJS uses | |---|---| | DOM `EventTarget` (elements, `window`, `document`) | `addEventListener` / `removeEventListener` | | Node-style emitter | `addListener` / `removeListener` | | jQuery-style emitter | `on` / `off` | | Array-like of targets (`NodeList`, `HTMLCollection`) | one listener per element, merged into one stream | For DOM targets the third argument is passed through to the listener calls: `capture`, `passive` (useful for `scroll` and `touchmove`, telling the browser the handler will not call `preventDefault()`) and `once`. If the target matches none of these shapes, `fromEvent` throws `TypeError('Invalid event target')` at call time. When a handler receives several arguments, as some Node-style emitters do, `fromEvent` emits them as an array; a DOM event emits just the event object. ## Typing the event RxJS does not map an event name to a specific event type, so `fromEvent(button, 'click')` is typically written with a type argument - `fromEvent<MouseEvent>(button, 'click')`, `fromEvent<KeyboardEvent>(input, 'keydown')` - to give the subscriber access to properties such as `clientX` or `key`. The type argument is only an assertion for the compiler: - it does not filter events at runtime; - a mismatched name and type (`fromEvent<KeyboardEvent>(el, 'click')`) compiles but lies to the subscriber; - keeping the name and the type next to each other in one helper makes such mismatches easy to spot. ## The click-stream scenario A typical interview prompt is "turn the refresh button's clicks into a source". The answer is a lazily created stream of `MouseEvent`s whose lifetime is tied to its subscription: 1. create `refreshClicks$ = fromEvent<MouseEvent>(button, 'click')`; 2. transform it with operators - mapping each click to a request, for example; 3. make sure the subscription ends when the screen goes away. In an Angular component, template event bindings such as `(click)` are the usual way to handle clicks, and Angular's own helpers for ending subscriptions belong to its RxJS interop; `fromEvent` fits when you need a stream of events from something outside a template, such as `window` or `document`. ## Summary for the interview - Lazy: a listener is added per subscription, removed on unsubscribe. - Emits the event object; never completes. - Third argument is listener options; `once` does not complete the stream. - Works with DOM, Node-style and jQuery-style targets and with lists of elements.
- In RxJS, why does fromEvent(el, 'click', { once: true }) not complete after the first click?`once` is a listener option handed to the browser, which removes the listener after one event. RxJS is not notified, and `fromEvent` never calls `complete()` on its own, so the Observable emits one event and then stays open. Use `fromEvent(el, 'click').pipe(take(1))` instead: `take(1)` completes and unsubscribes, which removes the listener.
- In RxJS, what does fromEvent do when the target is a NodeList of buttons?A `NodeList` is array-like, so `fromEvent` subscribes to each element with its own listener and merges the events into one stream. Unsubscribing removes every listener. This saves writing a loop, but the list is read when subscribed, so elements added to the page later are not included.
fromEvent is like asking a doorman to phone you every time someone rings the bell: each person who asks gets their own calls, the calls never stop on their own, and they only end when you tell the doorman to stop phoning.
saying these in an interview costs you the question
- fromEvent completes after the first event is received.
- Calling fromEvent attaches the listener immediately, before anyone subscribes.
- Several subscribers to one fromEvent stream share a single listener.
- Passing { once: true } makes the fromEvent stream complete after one event.
- Unsubscribing leaves the listener attached until the element is removed.