Observable API
Limited availability
This feature is not Baseline because it does not work in some of the most widely-used browsers.
Experimental: This is an experimental technology
Check the Browser compatibility table carefully before using this in production.
The Observable API provides a mechanism for handling streams of values, including asynchronous events. You can declare a pipeline to filter and transform these values, subscribe to receive them, and unsubscribe when they are no longer needed.
Concepts and usage
>Events and reactive programming
Events are fundamental to web development and JavaScript programming at large. Events originate from EventTarget objects:
element.addEventListener("click", handler1);
element.addEventListener("click", handler2);
When the user clicks on the element, the element, controlled by the browser, is responsible for pushing a notification to each of its event listeners, which are handler1 and handler2 in this case. The notification is an Event object providing further context and data about the event. The handler then carries out some action in response to the event.
This paradigm is known as reactive programming, where actions are carried out passively in response to a trigger. Abstractly, there are four main kinds of reactive programming:
- Single pull: The initiator receives a single value. Normal functions implement this: in
const result = action();, the caller of theaction()function is the initiator, which pulls a single valueresultfrom the function. - Single push: The initiator sends a single value, and the receiver waits before it receives that value. Promises implement this: in
promise.then((value) => { ... });, the promise implementation is the initiator, which calls the callback function exactly once when the promise is fulfilled, sending a single value. - Multiple pulls: The initiator pauses and resumes execution of the data source, eventually receiving multiple values. Iterators implement this: in
const result1 = iterator.next(); const result2 = iterator.next();, the caller of thenext()method is the initiator, which pulls multiple values from the iterator. - Multiple pushes: The initiator sends multiple values, and the receiver pauses execution in between receiving those values. Events implement this: in
element.addEventListener("click", handler);, the element is the initiator, which calls thehandlerfunction multiple times as the user clicks on the element, sending multiple event objects.
Composing event streams
The problem with the addEventListener() model is that there's no neat way to compose event handlers. Each handler is executed in isolation and doesn't inherently have knowledge about the other events in a sequence. For example, if you want to do something specifically for a mousemove event that happens after a mousedown event, you have to manage any shared state yourself, leading to complex and fragile code.
The Observable API addresses this problem by declaratively creating and manipulating streams of events using methods such as Observable.map() and Observable.filter(). It doesn't fundamentally replace the event-driven model, but rather changes the way you organize your code, much like how promises changed the way asynchronous code is written compared to traditional callbacks.
Note:
The Observable API is not inherently related to the event API. While event handling is a major use case for observables, observables can represent any stream of data, not just events. Because of this, the observable API is actually more like a language primitive than a web-exclusive API, just like Promise or Iterator. It is specified outside of TC39 for historical reasons, like AbortController or streams.
Obtaining observables
In the Observable API, an observable represents a stream of values, and an observer receives its notifications through callbacks. The code that sends values is called the producer, while the code that makes use of these values is called the consumer. You typically write the consumer code by using methods on the Observable interface to transform and consume values from observables. If you are the implementor of an observable, you write the producer code by using the Subscriber interface to send values to the observers. There are three main ways to obtain observables:
- The
EventTarget.when()method returns anObservablerepresenting a stream of events fired on theEventTarget. You may also have libraries that return observables. - You can create your own custom observables using the
Observable()constructor. - You can convert objects such as promises and iterables into observables using the static
Observable.from()method.
Transforming, subscribing, and unsubscribing
An observable can be transformed using various methods that return new observables, such as Observable.map() and Observable.filter().
To start receiving values from an observable, you subscribe to it using the Observable.subscribe() method, or aggregate all values using promise-returning methods like Observable.reduce(). Observables are lazy — they don't start producing values until they have at least one subscriber.
You can also unsubscribe from an observable using an AbortController or certain methods like Observable.takeUntil().
Let's consider a brief example:
document.body
.when("mousedown")
.filter((e) => e.target.matches("body"))
.map((e) => ({ x: e.clientX, y: e.clientY }))
.subscribe((p) => {
console.log(`${p.x},${p.y}`);
});
In this snippet, the page's <body> element is an EventTarget. We obtain a stream of mousedown events fired on it using the when() method.
We then specify a pipeline:
Observable.filter()filters the events passed through the pipeline to only events fired on the<body>element (tested using theElement.matches()method) and not its descendants.Observable.map()maps the firedmousedownevent objects to new objects containing the coordinates of the mouse cursor when the event was fired.Observable.subscribe()subscribes to the observable. Its callback logs the mouse coordinates to the console each time amousedownevent passes the filter.
You may notice that this paradigm is very similar to iterators, where we can also transform with map() and filter() and consume with next() and toArray(). Indeed, both observables and iterators represent streams of data; the key difference, as previously stated, is that iterators are pull-based (the consumer decides when to receive values), while observables are push-based (the producer decides when to send values).
See Using observables and Creating custom observables for more information on the above concepts.
Interfaces
Observable-
Represents a stream of values that can be subscribed to.
Subscriber-
Represents a subscription to a stream of observable values, and contains methods to manage the lifecycle of that subscription.
Extensions to other interfaces
EventTarget.when()-
Returns an observable representing a stream of events that will be fired on the
EventTarget.
Examples
See Using observables and Creating custom observables for complete examples.
Specifications
| Specification |
|---|
| Unknown specification> |