The Lit Team recently released Lit 2.0, more than two years after Lit 1. Lit 2 features a new API for custom directives that include asynchronous directives. Lit 2 users will also be able to use reactive controllers to encapsulate reusable reactive logic.
Lit 2 is a major update coming two years after the first Lit 1, which was released in February 2019. Lit 2.0 is slightly below 6KB according to bundlephobia. While Lit 2 comes with new features and enhancements, the Lit team emphasized that Lit 2 should be mostly backward compatible:
Lit 2 adds a number of new features and enhancements while maintaining backward compatibility; in most cases, Lit 2 should be a drop-in replacement for previous versions.
Lit 2 features a new API for custom directives. Custom directives are objects that expose methods with which an API user can control how Lit renders expressions.
// Renders attribute names of parent element to textContent
class AttributeLogger extends Directive {
attributeNames = '';
update(part: ChildPart) {
this.attributeNames = (part.parentNode as Element).getAttributeNames?.().join(' ');
return this.render();
}
render() {
return this.attributeNames;
}
}
const attributeLogger = directive(AttributeLogger);
const template = html`<div a b>${attributeLogger()}</div>`;
// Renders: `<div a b>a b</div>`
The previous example showcases the render
and update
methods of the Directive
class that user-defined directives must extend. The update
method enables imperative DOM access while the render
method can be used for declarative rendering. As Lit leverages tagged templates, the update
method is passed a Part
object with an API for directly managing the DOM elements associated with the underlying template expression.
Lit 2’s asynchronous directives enable updating the DOM asynchronously:
class ResolvePromise extends AsyncDirective {
render(promise: Promise<unknown>) {
Promise.resolve(promise).then((resolvedValue) => {
// Rendered asynchronously:
this.setValue(resolvedValue);
});
// Rendered synchronously:
return `Waiting for promise to resolve`;
}
}
export const resolvePromise = directive(ResolvePromise);
As asynchronous directives may be used in connection with external resources that require lifecycle management, the AsyncDirective
class exposes additional lifecycle methods (e.g., disconnected
, reconnected
).
Lit 2 adds a new reuse and composition strategy termed reactive controllers. Lit 2’s documentation explains reactive controllers as follows:
A reactive controller is an object that can hook into a component’s reactive update cycle. Controllers can bundle state and behavior related to a feature, making it reusable across multiple component definitions.
You can use controllers to implement features that require their own state and access to the component’s lifecycle; such as handling global events like mouse events, managing asynchronous tasks like fetching data over the network, or running animations.
Reactive controllers are not strictly compositional in the sense that they do not build a target component from other source components. Instead, they are closer to plug-ins in that they extend existing components with user-defined behavior. They are also similar to React Hooks in that they allow encapsulating custom pieces of logic or behavior in a conveniently reusable way.
The reactive controller and the host component are linked by a bidirectional API: the controller can access methods and fields on the component, and the component can access methods and fields on the controller. An example of a reactive controller that adds mouse tracking behavior to a host component is as follows:
import {LitElement, html} from 'lit';
import {MouseController} from './mouse-controller.js';
class MyElement extends LitElement {
mouse = new MouseController(this);
render() {
return html`
<h3>The mouse is at:</h3>
<pre>
x: ${this.mouse.pos.x}
y: ${this.mouse.pos.y}
</pre>
`;
}
}
customElements.define('my-element', MyElement);
The reactive controller MouseController
is used to enhance my-element
with the mouse tracking functionality implemented in the controller. The host component has access to the pos.x
and pos.y
API exposed by the reactive controller. The controller code is as follows:
export class MouseController {
host;
pos = {x: 0, y: 0};
_onMouseMove = ({clientX, clientY}) => {
this.pos = {x: clientX, y: clientY};
this.host.requestUpdate();
};
constructor(host) {
this.host = host;
host.addController(this);
}
hostConnected() {
window.addEventListener('mousemove', this._onMouseMove);
}
hostDisconnected() {
window.removeEventListener('mousemove', this._onMouseMove);
}
}
The previous code showcases the methods (e.g., hostConnected
, hostDisconnected
) tying to the host component lifecycle and the imperative update this.host.requestUpdate()
that the reactive controller must perform to trigger the rendering of the host component. The constructor for the reactive controller allows the reactive controller to hold a reference to the host component. As the host component and the reactive controller hold a reference of each other, they can cooperate to realize some defined behavior.
Lit self-describes as a simple library for building fast, lightweight web components. Developers may review Lit’s updated documentation online. The documentation site features an upgrade guide, a tutorial, and an interactive playground.
Lit is open-source software distributed under the BSD-3 clause license. Contributions are welcome and must follow the contributing guidelines.