Ng-News 25/15: Native Observables
Native Observables are now available in Chrome 135, offering deep integration into the Web API with .when() methods and default multicasting behavior. Careful: They differ a little bit from RxJS Observables in structure and behavior (e.g., Promise-returning methods, AbortController for cancellation). Native Observables Native Observables have landed in Chrome 135 — but don’t confuse them with the RxJS Observables we’re used to. Before diving into the differences, let’s first look at how they’re integrated into the existing Web APIs. Traditionally, we use addEventListener() to register callbacks on DOM elements. With native Observables, these same elements now expose a when() method that returns an Observable — a sign of how deeply integrated this feature is. // until now document.addEventListener('mousemove', console.log); // with native Observables document.when('mousemove').subscribe(console.log); Key Differences from RxJS Observables 1. Multicasting Behavior by Default Native Observables are shared by default. That means they’re multicasting, only activate on the first subscriber, and do not replay past values. This is similar to RxJS's share() operator — but importantly, not shareReplay(). Given the following code in RxJS: const numbers$ = new Observable((subscriber) => { subscriber.next(1); subscriber.next(2); setTimeout(() => { subscriber.next(3); }); }); numbers$.subscribe((n) => console.log(`Sub 1: ${n}`)); numbers$.subscribe((n) => console.log(`Sub 2: ${n}`)); We would get the following output: Sub 1: 1 Sub 1: 2 Sub 2: 1 Sub 2: 2 Sub 1: 3 Sub 2: 3 With native Observables, though, the same code would produce the following output: Sub 1: 1 Sub 1: 2 Sub 1: 3 Sub 2: 3 That is because the first subscription triggers the execution within the Observable and numbers 1 and 2 are emitted synchronously. Only value 3 emits in an asynchronous task and is therefore also consumed by the second subscriber. 2. Operators as Methods Instead of using pipe() like in modern RxJS, native Observables go back to the roots: operators like map() or filter() are methods directly on the Observable instance. This would be the RxJS-valid code: // RxJS Observables const numbers$ = new Observable((subscriber) => { subscriber.next(2); subscriber.next(4); subscriber.next(8); }); numbers$ .pipe( map((n) => n * 2), tap((n) => console.log(`tap: ${n}`)), filter((n) => n { subscriber.next(2); subscriber.next(4); subscriber.next(8); }); numbers$ .map((n) => n * 2) .inspect((n) => console.log(`tap: ${n}`)) //

Native Observables are now available in Chrome 135, offering deep integration into the Web API with .when()
methods and default multicasting behavior. Careful: They differ a little bit from RxJS Observables in structure and behavior (e.g., Promise-returning methods, AbortController for cancellation).
Native Observables
Native Observables have landed in Chrome 135 — but don’t confuse them with the RxJS Observables we’re used to. Before diving into the differences, let’s first look at how they’re integrated into the existing Web APIs.
Traditionally, we use addEventListener()
to register callbacks on DOM elements. With native Observables, these same elements now expose a when()
method that returns an Observable — a sign of how deeply integrated this feature is.
// until now
document.addEventListener('mousemove', console.log);
// with native Observables
document.when('mousemove').subscribe(console.log);
Key Differences from RxJS Observables
1. Multicasting Behavior by Default
Native Observables are shared by default. That means they’re multicasting, only activate on the first subscriber, and do not replay past values. This is similar to RxJS's share()
operator — but importantly, not shareReplay()
.
Given the following code in RxJS:
const numbers$ = new Observable((subscriber) => {
subscriber.next(1);
subscriber.next(2);
setTimeout(() => {
subscriber.next(3);
});
});
numbers$.subscribe((n) => console.log(`Sub 1: ${n}`));
numbers$.subscribe((n) => console.log(`Sub 2: ${n}`));
We would get the following output:
Sub 1: 1
Sub 1: 2
Sub 2: 1
Sub 2: 2
Sub 1: 3
Sub 2: 3
With native Observables, though, the same code would produce the following output:
Sub 1: 1
Sub 1: 2
Sub 1: 3
Sub 2: 3
That is because the first subscription triggers the execution within the Observable and numbers 1 and 2 are emitted synchronously. Only value 3 emits in an asynchronous task and is therefore also consumed by the second subscriber.
2. Operators as Methods
Instead of using pipe()
like in modern RxJS, native Observables go back to the roots: operators like map()
or filter()
are methods directly on the Observable instance.
This would be the RxJS-valid code:
// RxJS Observables
const numbers$ = new Observable<number>((subscriber) => {
subscriber.next(2);
subscriber.next(4);
subscriber.next(8);
});
numbers$
.pipe(
map((n) => n * 2),
tap((n) => console.log(`tap: ${n}`)),
filter((n) => n < 10),
)
.subscribe(console.log);
The same code but with native Observables:
// Native Observables
const numbers$ = new Observable((subscriber) => {
subscriber.next(2);
subscriber.next(4);
subscriber.next(8);
});
numbers$
.map((n) => n * 2)
.inspect((n) => console.log(`tap: ${n}`)) // <-- that's tap()
.filter((n) => n < 10)
.subscribe(console.log);
3. Promise-returning Methods
Some operators (like first()
, last()
, reduce()
, and forEach()
) return Promises, not Observables. They would also replace the necessity for an explicit subscribe
.
This makes them easier to use in async/await flows.
In RxJS, there are utility functions to map an Observable into a Promise:
// RxJS
const countdown = new Observable((subscriber) => {
let counter = 1;
const intervalId = setInterval(() => {
subscriber.next(counter++);
if (counter > 5) {
clearInterval(intervalId);
subscriber.complete();
}
});
});
await lastValueFrom(countdown.pipe(tap(console.log)));
console.log('ended');
With native Observables, last()
returns already the Promise instead an Observable.
// Native Observables
const countdown = new Observable((subscriber) => {
let counter = 1;
const intervalId = setInterval(() => {
subscriber.next(counter++);
if (counter > 5) {
clearInterval(intervalId);
subscriber.complete();
}
});
});
await countdown.inspect(console.log).last();
console.log("ended");
4. AbortController
instead unsubscribe()
Unsubscribing is handled via AbortController
, similar to how we cancel fetch()
requests. Angular’s new resource()
function uses this same pattern as well, so this approach should already feel familiar.
In RxJS, we usually use unsubscribe
, when the subscription does not want to receive values anymore.
// RxJS
const numbers$ = new Observable<number>((subscriber) => {
subscriber.next(1);
subscriber.next(2);
});
const subscription = numbers$.subscribe((value) => {
console.log(value);
if (value >= 1) {
console.log('aborting/unsubscribing (even synchronously)');
}
});
subscription.unsubscribe();
Since native Observables are much more integrated into the Web API, the unsubscription is done with the AbortController
:
// Native Observables
const numbers$ = new Observable((subscriber) => {
subscriber.next(1);
subscriber.next(2);
});
const abortController = new AbortController();
numbers$.subscribe(
(value) => {
console.log(value);
if (value >= 1) {
console.log("aborting/unsubscribing (even synchronously)");
abortController.abort(); // <-- "unsubscribe" here
}
},
{ signal: abortController.signal },
);
5. Teardown via addTeardown()
The constructor function of an Observable no longer returns a teardown function. Instead, teardown logic is registered using addTeardown()
.
RxJS Version:
// RxJS Observable
const numbers$ = new Observable<number>((subscriber) => {
subscriber.next(1);
subscriber.next(2);
return () => console.log('complete inside the observable');
});
With native Observables, the teardown function will be more obvious:
// Native Observables
const numbers$ = new Observable((subscriber) => {
subscriber.addTeardown(() => {
console.log("completes inside the observable");
subscriber.complete();
});
subscriber.next(1);
subscriber.next(2);
});
Will Observables Become a Language-Level Feature?
It’s still uncertain whether Observable will become a language-level standard (like Promise) — or if it will remain a browser-level Web API. That distinction will shape how deeply integrated they become across platforms.
https://github.com/wicg/observable?tab=readme-ov-file#standards-venue
What About RxJS?
RxJS 8 development had paused while waiting for native Observable readiness. Now that they’ve landed, RxJS will move forward with integrating them — including shims for environments where they aren’t available yet.
https://github.com/ReactiveX/rxjs/issues/6367
And Angular Signals?
No change there. Signals are for state, while Observables are for events — like DOM events or async completions. Observables can still be useful to model these event triggers, but state management in Angular is moving to Signals, not back to BehaviorSubject.
To experiment with the examples, fine a ready to use starter at https://stackblitz.com/edit/stackblitz-starters-ybgwzhbc
Framework Waves
At the dotJS conference, Sarah Drasner gave a talk on how modern frontend frameworks have been continuously influencing one another.
She pointed out, for instance, that Qwik’s approach to resumability was likely inspired by Wiz — Google’s internal framework that is now being integrated into Angular.
Naturally, she focused on Angular’s current role in this evolving landscape. With features like incremental hydration, Angular is now in a position to influence other frameworks in return.
She concluded her talk by mentioning a new open-source library called tsurge, which she suggested could complement Angular’s ng update capabilities.
NgRx 19.1
NgRx 19.1 was released. It comes with a new features for the SignalStore in the areas of
- Testing
- Custom Features
withEntities
https://github.com/ngrx/platform/blob/main/CHANGELOG.md#1910-2025-04-01