Building Reactive Apps with RxJS and Lit

Building Reactive Apps with RxJS and Lit

Published:

Do you wonder whether you should combine RxJS with Lit? And if so, what are the integration options? We’ll answer those questions in this post.

Cover image

Is Lit + RxJS a good idea?

RxJS can easily be used within your Lit Web Components, as we will see below. But is it reasonable to combine those two?

In my post Should I learn RxJS? , I already stated that it does not make sense to include RxJS into a mostly static website, because of the payload that comes with the library. You can have a more detailed look at the export sizes of RxJS at bundlephobia .

If you think about Web Components, what quickly comes to mind are reusable components that can be shared between multiple pages and apps, independent of the frameworks used on those pages and apps.

With Lit’s reactive properties and fast templating, you can easily create fast, resource efficient Web Components.

If you are building a whole set of reusable components, the relative payload of the libraries becomes smaller and smaller. But, depending on the set of operators used, RxJS can quickly increase the size of library code in your bundle.

With Lit, you can also build entire applications. In this case, the bundle size still matters, but it tends to matter less than on a static page.

So, depending on what you are building with those components, you should be careful about what libraries you are buying into.

That said, let’s take a look at how we can actually integrate RxJS to our Lit components.

Integrating RxJS into Lit Components

We will use a simple source Observable count$ for the showcases below.

export const count$ = timer(0, 1000);

There are two ways how to manage Subscriptions to RxJS Observables within our components — using lifecycle hooks or directives.

Using Lifecycle Hooks

Many component frameworks provide component lifecycle hooks. Those are called within the lifecycle of a certain component. Often we want to subscribe to an Observable the whole time a component is connected to the DOM.

In Lit, we have the hooks connectedCallback() and disconnectedCallback() available for this.

An implementation of this approach can look like this:

@customElement("count-lifecycle-hooks")
export class CountLifecycleHooks extends LitElement {
  @state() count = 0;

  #sub?: Subscription;

  render() {
    return this.count;
  }

  connectedCallback(): void {
    super.connectedCallback();
    this.#sub = count$.subscribe(
      (count) => (this.count = count),
    );
  }

  disconnectedCallback(): void {
    super.disconnectedCallback();
    this.#sub?.unsubscribe();
  }
}

While this is a solution, we have the same issues as in other component libraries like React: It’s quite verbose, and we have the risk of accidentally creating memory leaks if we forget to unsubscribe again.

We can eliminate both of those issues by using a directive.

Using a Directive

If we want to change template parts independent of the reactive update cycle of the surrounding component, we can use an async directive. It can emit template parts at any time.

An example of an observe directive can be found in the docs of Lit . Here is an adapted version for RxJS.

class ObserveDirective extends AsyncDirective {
  observable: Observable<unknown> | undefined;

  #sub?: Subscription;

  render(observable: Observable<unknown>) {
    if (this.observable !== observable) {
      this.#sub?.unsubscribe();
      this.observable = observable;
      if (this.isConnected) {
        this.subscribe(observable);
      }
    }
    return noChange;
  }

  subscribe(observable: Observable<unknown>) {
    this.#sub = observable.subscribe((v: unknown) => {
      this.setValue(v);
    });
  }

  disconnected() {
    this.#sub?.unsubscribe();
  }
  
  reconnected() {
    this.subscribe(this.observable!);
  }
}
export const observe = directive(ObserveDirective); 

The directive’s lifecycle is connected to the lifecycle of its surrounding component. So when the holding component gets disconnected from the DOM, also the directive’s disconnected() hook is called.

We can simply use our observe directive within the render function of any component.

@customElement("count-directive")
export class CountDirective extends LitElement {
  render() {
    return observe(count$);
  }
}

This is a clean way to integrate Observables into Lit components.

Not only are we left with concise code and our subscriptions are completely managed by the directive, we also can benefit from fine-grained reactivity: The render() method of the directive’s host component does not need to be re-executed if the Observable emits a new value.