Cory Rylan

My name is , Google Developer Expert, Speaker, Software Developer. Building Design Systems and Web Components.

Follow @coryrylan
RxJS

Using RxJS in Lit Web Components

Cory Rylan

- 4 minutes

Lit is a library for authoring Web Components. Lit provides reactive templating and utilities to make reusable Web Components easy. When managing a large application with complex dataflows tools like RxJS can help manage data. RxJS is an Observable library which helps apps manage complex data flows.

In this blog post we will learn how to leverage Lit and its decorators API to make using RxJS Obervables easy to manage within our components.

Note: This advanced concept post assumes some basic knowledge of Lit and Rxjs

In this example we will be creating a small counter component/widget with Lit and RxJS. Then once working we will refactor our code to leverage some specific APIs to manage RxJS subscriptions automatically.

Example counter widget built with RxJS and a Lit based Web Component

Lit can update/rerender property updates using the @property or @state decorator. We will use the @state decorator to track our counter value to render in the template.

import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { state } from 'lit/decorators/state.js';
import { interval, Subject } from 'rxjs';
import { scan, map } from 'rxjs/operators';

@customElement('my-element')
class MyElement extends LitElement {
  @state() value = 0;
  click$ = new Subject();
 
  render() {
    return html`
      <button @click=${e => this.click$.next(e)} value="-1">-</button>
      <p>value: ${this.value}</p>
      <button @click=${e => this.click$.next(e)} value="1">+</button>
    `;
  }
  
  constructor() {
    super();
    this.click$.pipe(
      map(e => parseInt(e.target.value)),
      scan((p, n) => p + n, 0)
    ).subscribe(value => this.value = value);
  }
}

Using an RxJS Subject we can trigger events from our buttons as well as subscribe to them. In this example on click we dispatch a new event to our subject via next(). Within our constructor we can subscribte to that same subject and use RxJS operators like map and scan to total up our values as each event comes into our subscription.

While this example works, we still need to unsubscribe from our Obervable/Subjects when components are removed.

import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { state } from 'lit/decorators/state.js';
import { interval, Subject, Subscription } from 'rxjs';
import { scan, map } from 'rxjs/operators';

@customElement('my-element')
class MyElement extends LitElement {
  @state() value = 0;
  subscription: Subscription;
  click$ = new Subject();
 
  render() {
    return html`
      <button @click=${e => this.click$.next(e)} value="-1">-</button>
      <p>value: ${this.value}</p>
      <button @click=${e => this.click$.next(e)} value="1">+</button>
    `;
  }
  
  constructor() {
    super();
    // assign the subscription to unsubscribe later
    this.subscription = this.click$.pipe(
      map(e => parseInt(e.target.value)),
      scan((p, n) => p + n, 0)
    ).subscribe(value => this.value = value);
  }
  
  disconnectedCallback() {
    super.disconnectedCallback();
    // when removed from the DOM, unsubscribe
    this.subscription.unsubscribe();
  }
}

In the disconnectedCallback we can unsubscribe from our Observables to ensure we have no memory leaks within our component. While this demo is small it shows its possible to use RxJS in a Lit based Web Component. However we can leverage a the Lit Directive API to manage our subscriptions for us.

Lit Directives

Lit Directives provide another way to interact with DOM and other Web Components. Directives provide various lifecyle hooks to manage DOM interactions.

In this example we will make a basic observe directive that will manage our RxJS subscriptions automatically.

<p>value: ${observe(this.count$)}</p>

Lit has several various Directive API types, but for our use case we will use the AsyncDirective.

import { AsyncDirective } from 'lit/async-directive.js';
import { Directive, directive, EventPart, DirectiveParameters } from 'lit/directive.js';
import { Observable, Subject, Subscription } from 'rxjs';

class ObserveDirective extends AsyncDirective {
  #subscription: Subscription;

  render(observable: Observable<unknown>) {
    this.#subscription = observable.subscribe(value => this.setValue(value));
    return ``;
  }
  
  disconnected() {
    this.#subscription?.unsubscribe();
  }
}

export const observe = directive(ObserveDirective);

With the async directive we can control the rendered output of a value in our template. By subscribing to our Observable passed to the directive we can all the setValue method when a new event occurs. The setValue method will update the value in the template whenever called. This enables us to control exactly when the value should update from our Observable.

This directive API also provides several lifecycle hooks like disconnected. When the disconnected hook is called we can unsubscribe from our Observable and prevent any memory leak.

import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { observe } from './rx-directive.js';
import { interval, Subject } from 'rxjs';
import { scan, map } from 'rxjs/operators';

@customElement('my-element')
class MyElement extends LitElement {
  click$ = new Subject();
  count$ = this.click$.pipe(
    map(e => parseInt(e.target.value)),
    scan((p, n) => p + n, 0)
  );

  render() {
    return html`
      <p>${Math.random()}</p>
      <button @click=${e => this.click$.next(e)} value="-1">-</button>
      <p>value: ${observe(this.count$)}</p>
      <button @click=${e => this.click$.next(e)} value="1">+</button>
    `;
  }
}

Now using our observe directive we reduce the amount of code needed to use Observables in Lit without manually managing subscriptions. Checkout the full demo below which also includes an aditonal directive for emitting events!

View Demo Code   
Twitter Facebook LinkedIn Email
 

No spam. Short occasional updates on Web Development articles, videos, and new courses in your inbox.

Related Posts

Angular

Angular HTTP Pending Request Pattern

Learn how to display HTTP request status messages to users using RxJS Observables.

Read Article
RxJS

RxJS Observables versus Subjects

A tutorial on the differences between Observables and various Subjects in RxJS.

Read Article
RxJS

JavaScript Promises Versus RxJS Observables

Learn the differences between JavaScript Promises and RxJS Observables. Learn why Observables provide more functionality than a typical Promise.

Read Article