Motivation
Functions are the simplest unit of composition and the first tool a developer leverages to accomplish almost any task. lit empowers developers to write shareable interoperable code that creates rich interactive interfaces. Minimizing the distance and friction between starting with a function and ending with a great UI serves that goal.
There are 2 main pieces of this puzzle: (1) describing rendering, (2) managing reactive state.
Describing rendering: Lit's html and css TTL syntaxes are excellent at this, and can easily be returned from a function.
Managing reactive state:
- Lit's reactive properties provide an excellent mechanism for managing reactive state and make sense to use when creating custom elements that extend
LitElement. If a dev started with a function, they must move to a class, and this is fine when it makes sense.
- However, the new @lit-labs/preact-signals library provides an alternative for simpler use cases and it more seamlessly builds on top of functions.
- Signals are reactive state containers. Their reactivity does require using them within a scope that records dependencies (aka
effects), but the lit signals package provides SignalWatcher and watch to deal with this.
- The one remaining problem is having a convenient way to create signals within a function. They can certainly be passed in as arguments, but what if they are internal the ui being managed by the function?
This last issue is addressed below...
Example
Consider creating a simple counter:
In a LitElement, it looks like:
@state()
accessor count = 0;
render() {
return html`<button @click=${() => this.count++}>${this.count}</button>`;
}
With a function using signals, this can be:
const renderCounter = () => html`<button @click=${() => count.value++}>${count}</button>`;
But this doesn't work because we need to get the count signal from somewhere.
How
React solves this problem with hooks, but these have tradeoffs. Lit can make this more straightforward by leveraging the fact that all Lit templates are rendered into persistent "parts," and access to them is provided via the directives API.
So, all we need is a simple directive that can initialize state. Here's the updated counter example:
const renderCounter = (use) => {
const count = use.state(() => signal(0));
return html`<button @click=${() => count.value++}>${count}</button>`;
}
Then use it like this:
return html`counter: ${stateful(renderCounter)}`
See working prototype.
The stateful directive provides a state method which memoizes the result of its argument.
References
Motivation
Functions are the simplest unit of composition and the first tool a developer leverages to accomplish almost any task.
litempowers developers to write shareable interoperable code that creates rich interactive interfaces. Minimizing the distance and friction between starting with a function and ending with a great UI serves that goal.There are 2 main pieces of this puzzle: (1) describing rendering, (2) managing reactive state.
Describing rendering: Lit's
htmlandcssTTL syntaxes are excellent at this, and can easily be returned from a function.Managing reactive state:
LitElement. If a dev started with a function, they must move to a class, and this is fine when it makes sense.effects), but the lit signals package providesSignalWatcherandwatchto deal with this.This last issue is addressed below...
Example
Consider creating a simple counter:
In a LitElement, it looks like:
With a function using signals, this can be:
But this doesn't work because we need to get the
countsignal from somewhere.How
React solves this problem with hooks, but these have tradeoffs. Lit can make this more straightforward by leveraging the fact that all Lit templates are rendered into persistent "parts," and access to them is provided via the directives API.
So, all we need is a simple directive that can initialize state. Here's the updated counter example:
Then use it like this:
See working prototype.
The
statefuldirective provides astatemethod which memoizes the result of its argument.References