Using RxJS in Lit Web Components
Cory Rylan
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.
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](/assets/images/posts/2022-05-30-using-rxjs-in-lit-web-components/counter-widget-with-rxjs-and-lit-web-component.png)
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!