DEV Community

Cover image for WICG Observables, RxJS v7, v8 and the Observable that doesn't exist yet
Dario Mannu
Dario Mannu

Posted on • Edited on

WICG Observables, RxJS v7, v8 and the Observable that doesn't exist yet

A lot of interesting things are happening right now.

RxJS 7 rocks, RxJS 8 is in alpha, it appears to be usable and so are the new Native Observables which are available in Chrome Today (May need turningn on at: chrome://flags/#observable-api).

Can we use them together?
Short answer, kind of no. Technically they're all Observables, so should speak the same language, right? It's just .subscribe, .next, .error, .complete, and voilà...

Well, almost. Except RxJS makes some additional effort to ensure it's dealing with true Obsevables and not the "cheap imports" 😂.

It diligently checks for Symbol.observable or @@observable to be present, so you could technically monkey-patch those into the DOM Observable by doing Observable.prototype['@@observable'] = function(){ return this }, but... even if you succeed and you manage to plug both together via document.when('click').subscribe(new Subject()), it will fail again because RxJS streams make references to their own this, internally, which now will point elsewhere... so it breaks.

Hard luck, we need a custom bridge that subscribes to the Native Observable and forwards data to RxJS land.

Great, suppose we did that, sure, it would work. You would suddenly be able to do something like the following, provided you have this wrap silly function, done:

// RxJS 8.x
const clickCount = rx(
  wrap(document.when('click')),
  scan(x=>x+1, 0),
);

clickCount.subscribe(doSomething);
Enter fullscreen mode Exit fullscreen mode

Anyway, whilst the above could already qualify as some kind of news, it's not the really interesting part, at all, yet!

The interesting part

The interesting part here comes when we use Observables in the real world, in real applications, which are typically created using frameworks or UI libraries.

Consider the case of a click-counter button, using Observables, inside a JavaScript "Component".

import { Subject, scan } from 'rxjs';
import { rml } from 'rimmel';

const Component = () => {
  const counter = new BehaviorSubject(0).pipe(
    scan(x=>x+1)
  );

  return rml`
    <button onclick="${counter}">hit me</button>
    Hit Count: <span>${counter}</span>
  `;
}
Enter fullscreen mode Exit fullscreen mode

Now, with Native DOM Observables we have a couple of interesting problems. Subject doesn't exist, BehaviorSubject doesn't either.

In addition to that, it doesn't even have a .pipe() method to pass operators in.

Lastly, its native operators are all instance methods of the Observable class, instead of the plain operator functions we may have become accustomed to.

So, the big question is: how do you call an instance method of an object that... doesn't exist yet?

(You're probably lost at this point... I know, hold up)

The new way to create Observables in the DOM looks like element.when(eventName). It's a native call to the DOM.
However, we're now in a template, we're in a JavaScript Component and none of the HTML has been added to the DOM yet, so no call to .when() could have been possibly made!

And we want to call .map().inspect().filter() on it...

An oversight? RxJS used to sport the same interface until a few years ago (others like Bacon and Zen Observables still do), but to help tree-shaking, they've split all operator methods into operator functions, so now you can import just what you need, making your apps lighter. Great!

The Observature

So, back to our new situation, how do we solve that from within a component?
Sure, well, that's easy! We either get Subject and BehaviorSubject in the WICG proposal (spoiler: for now we won't), or... we get creative, hack the system and conceive something like a proxy that helps us pretend that the Native DOM Observable is there, even if it's not, so we can call its native operator methods. 🤪🤌🤯

I called it: The Observature. 😱

Observable + Future = Observature.

Observaturus is Latin for "someone who will observe", so if we force that into English, it should sound something like that.

OK, so what would it all look like in code?

import { rml } from 'rimmel';

const Component = () => {
  const counter = Observature(0)
    .scan(x=>x+1)
  ;

  return rml`
    <button onclick="${counter}">hit me</button>
    Count: <span>${counter}</span>
  `;
}
Enter fullscreen mode Exit fullscreen mode

Yay, look at that! We have something here: new Observature(0).scan(x=>x+1).
Let me explain this.

It's technically like creating a new BehaviorSubject(0).scan(x=>x+1) except for one thing: there's no BehaviorSubject anymore, yet it will start with 0. 😂

The Observature is just a proxy. It exposes methods from Observable and Observer for later subscription and binding (by a framework of UI library).

If you call .scan(fn), it will just remember to call .scan on the actual Observable it will be subscribed to, when the time comes.


So, what interesting stuff do Observatures bring?
First is the fact they're not actual Subjects, so when you run the code above, the operator function you provide will run at depth 1 or 2 in the stack. It could be lighter and faster than anything you've seen before, from click to sink. No, haven't run benchmarks yet and I'm not bothered, it's the concept that matters, for now.

Ah, another little note. There's no Observable.scan(), too, in the spec now, so one thing we can do is to monkey-patch at the moment, but again, those are just tiny implementation details. We have native Observables, that's the big deal!

To stay 100% native, for other use cases you can just use .map() and .filter(), but in my experience you can't live a proper life without .scan(), as well.

RxJS 8

Ok, so... the above was using native stuff, no RxJS.
What would it all look like in RxJS8?
(The actual best question is what it will look like in RxJS 9, btw, but that's one for another day)

The current obstacles are the same: native Observables aren't recognised by RxJS, so there's a little bit of work to do. It could all look something like this (or not look at all):

// npm install rxjs@next
import { rx, Subject, scan } from 'rxjs';
import { rml } from 'rimmel';

const Component = () => {
  // In rx8 the rx() function no longer exposes
  // the Observer interface when piping a Subject in
  // so we have to split the pipeline in two :(
  const counterInput = new Observature(0);
  const counterOutput = rx(
    counterInput,
    scan(x=>x+1),
  );

  return rml`
    <button onclick="${counter}">hit me</button>
    Count: <span>${counter}</span>
  `;
}
Enter fullscreen mode Exit fullscreen mode

Can we not use a Subject instead of an Observature?

That was my initial idea, as well, but consider the following:
if you run 100% native, your pipeline looks as follows:

source
  .when(event)
  .map(transform)
.subscribe(sink)
Enter fullscreen mode Exit fullscreen mode

which as we said, we can't do in a "component".

A Subject could indeed be used:

// 1. create your pipeline in your component
const stream = new Subject()
  .map(transform)
;

// then, on mount (either you or your framework) would do:

// 2. sink it down
stream.subscribe(sink)

// 3. source it up
target
  .when(event)
  .subscribe(stream)
;
Enter fullscreen mode Exit fullscreen mode

The issue here may not be so obvious, but we would introduce an unnecessary processing step:

source
  .when(event)
  --[ subscribed to]--> the Subject created above
  .map(transform)
.subscribe(sink)
Enter fullscreen mode Exit fullscreen mode

The Observature, on the other hand, adds an .addSource() method designed to connect a native Observable (target.when) to the first step of the pipeline, with no intermediary processing steps, so it would be 100% equivalent of doing target.when(event).map(firstStep) at mount time.

This should be good for performance and memory usage.

Summary

Observatures, first introduced by Rimmel.js are a Proxy-based walkaround to the fact that JavaScript UI Components need an Observable stream to be able to build Observable pipelines, which don't exist until mounting.

For now, you can play with DOM Observables using Observatures on this Stackblitz (relevant lines highlighted)

Rimmel is the first UI library with support to native Observables and the only one at the time of writing.

Drop a message to leave your thoughts.

Finally, if you like what you've seen here please don't forget to leave a Star in Github ⭐ so we can continue evolving it for you!

Learn More

Redis image

Short-term memory for faster
AI agents 🤖💨

AI agents struggle with latency and context switching. Redis fixes it with a fast, in-memory layer for short-term context—plus native support for vectors and semi-structured data to keep real-time workflows on track.

Start building

Top comments (0)

Gen AI apps are built with MongoDB Atlas

Gen AI apps are built with MongoDB Atlas

MongoDB Atlas is the developer-friendly database for building, scaling, and running gen AI & LLM apps—no separate vector DB needed. Enjoy native vector search, 115+ regions, and flexible document modeling. Build AI faster, all in one place.

Start Free

👋 Kindness is contagious

Dive into this thoughtful piece, beloved in the supportive DEV Community. Coders of every background are invited to share and elevate our collective know-how.

A sincere "thank you" can brighten someone's day—leave your appreciation below!

On DEV, sharing knowledge smooths our journey and tightens our community bonds. Enjoyed this? A quick thank you to the author is hugely appreciated.

Okay