JavaScript Signals proposal integration for Lit
npm install @lit-labs/signalsTC39 Signals Proposal integration for Lit.




> [!WARNING]
>
> This package is part of Lit Labs. It
> is published in order to get feedback on the design and may receive breaking
> changes or stop being supported.
>
> Please read our Lit Labs documentation
> before using this library in production.
>
> Give feedback: https://github.com/lit/lit/discussions/4779
>
> RFC: https://github.com/lit/rfcs/blob/main/rfcs/0005-standard-signals.md
Full documentation is available at
lit.dev/docs/data/signals/.
@lit-labs/signals integrates the TC39 Signals
Proposal with Lit's template system
and reactive update lifecycle. Signals used within an element's update
lifecycle, such as in a template, will cause the element to re-render when the
signal value changes. Signals can also be used for targetted or "pin-point" DOM
updates, which can update the DOM without running the entire render() method.
The TC39 Signals Proposal is a
proposal to add standard signals to the JavaScript language.
This is very exciting for web components, since it means that different web
components that don't use the same libraries can interoperably consume and
produce signals.
It also means that many existing state management systems and observability
libraries that might currently each require their own adapter to integrated with
the Lit lifecycle, might converge on using standard signals so that we only need
one Lit adapter, and eventually no adapter at all as support for signals is
directly added to Lit
Signals have several nice attributes for use with reactive components like Lit:
1. Signals are an easy way to create shared observable state - state that many
elements can use and update when it changes. This is great for things like a
game state that many components need to read.
2. Signals can be individually observed, and when used in a template binding,
can be handled so that they only update the DOM their bound to. These
targetted DOM updates don't re-render the entire template.
3. Standard signals are an observability interoperabiliy point that many
different libraries can use. Any library that produces signals will work with
any standard signal watcher.
4. Signals can be good for performance. Signals track dependencies and changes
so that only signals that miht have changed and have been read are
re-computd. This can help perform minimal computations and DOM updates when
doing small updates to large signal graphs or UIs.
5. Signal auto-tracking can reduce the need for component-specific lifecycle
APIs. For example, rather than having lifecycle callbacks for when updates
have happened, or when specific reactive properties have changed, any code
could create a reactive effect that simple accesses the signals it uses, and
is automatically re-run when they change.
6. Signals may allow for interoperable _synchronous_ and _batched_ DOM updates.
There are ways to respond to signal changes synchronously but also batched,
so if reactive properties were backed by signals, an element could re-render
itself once a batch of them had been updated. Elements could take care to
update children inside of batches, meaning entire subtrees could be updated
synchrously. The batching mechanism isn't standard yet, but could be an
extension to the proposal.
Signals are a natural fit for Lit: a LitElement render method is already
somewhat like a computed signal in that it is computed based on updates to
inputs (reactive properties).
The difference between Lit renders and signals is
that in Lit the data flow is push-based, rather than pull-based as in signals.
Lit elements react when changes are pushed into them, whereas signals
automatically subscribe to the other signals they access. But these approaches
are very compatible, and we can easily make elements subscribe to the signals
they access and trigger an update with an integration library like this one.
Like all Lit Labs packages, @lit-labs/signals package may change frequently,
have serious bugs, or not be maintained as well as Lit's core packages.
Additionally, this package depends on the API defined in the TC39 Signals
proposal and directly depends on the
Signals polyfill, which
add more potential sources of instability and bugs. The proposal may change, and
the polfyill may have bugs or serious performance issues. If multiple versions
of the polyfill are included on a page, interoperabiilty may fail.
As the Signals proposal and polyfill progress we will update this package. At
some point we will remove the dependency on the polyfill and assume the standard
signal APIs exist, and pages will have to install the polyfill if needed.
So @lit-labs/signals is not recommended for production use. If you choose to
use it, please thouroughly test and check the performance of your components
and/or app _at scale_, with the number of signals and component instances that
you expect in real-world usage.
Please file feedback and bugs with the Lit
project, the Signals
Proposal, and the Signals
polyfill a appropriate.
There are three main exports:
- The SignalWatcher mixin
- The watch() directive
- The html template tag, and withWatch() template tag factory
SignalWatcher is the core of signals integration. It's a mixin that makes an
element watch all signal accesses during the element's reactive update
lifecycle, then triggers an element update when signals change. This includes
signals read in shouldUpdate(), willUpdate(), update(), render(),updated(), firstUpdated(), and reactive controller's hostUpdate() andhostUpdated().
This effectively makes the the return result of render() a computed signal.
``ts
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {SignalWatcher, signal} from '@lit-labs/signals';
const count = signal(0);
@customElement('signal-example')
export class SignalExample extends SignalWatcher(LitElement) {
static styles = css
:host {
display: block;
}
;
render() {
return html
The count is ${count.get()}
;
} #onClick() {
count.set(count.get() + 1);
}
}
`Elements should not _write_ to signals in these lifecycle methods or they might
cause an infinite loop.
$3
The
watch() directive accepts a single Signal and renders its value,
subscribing to updates and updating the DOM when the signal changes. This allows
for very targeted updates of the DOM, which can be good for performance (but as
always, measure!).`ts
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {SignalWatcher, watch, signal} from '@lit-labs/signals';const count = signal(0);
@customElement('signal-example')
export class SignalExample extends SignalWatcher(LitElement) {
static styles = css
; render() {
return html
The count is ${watch(count)}
;
} #onClick() {
count.set(count.get() + 1);
}
}
`watch() updates do not trigger the Lit reactive update cycle. However, they
are batched and run in coordination with reactive updates. When a watched
signal changes, if a reactive update is pending, the watched signal update
renders with the update; otherwise, it renders in a batch with any other
watched signals. Other changes, to reactive properties or signals accessed
outside of watch(), trigger reactive updates as usual.You can mix and match targeted updates with
watch() directive and
auto-tracking with SignalWatcher. When you pass a signal directly to watch()
it is not accessed in a callback watched by SignalWatcher, so an update to
that signal will only cause a targeted DOM update and not an full template
render.> [!NOTE]
>
>
>
> The value passed to
watch must be a signal. If it isn't, it can be
> converted to one using computed. For example
> ${watch(computed(() => list.get().map(item => item))}.$3
SignalWatcher also exposes an effect(callback) method that allows targeted
reactions to signal changes independent of but coordinated with the reactive
update lifecycle. This provides an easy mechanism to react generally to signals
used only with watch that do not trigger a reactive update. By default,
the effect is run after any DOM based updates are rendered, including
the element's reactive update cycle, if it is pending when the effect would
trigger. Adding an options argument with beforeUpdate: true allows effects
to run before any DOM is updated. For example,
this.effect(() => console.log(this.aSignal.get())), {beforeUpdate: true});$3
This package also exports an
html template tag that can be used in place of
Lit's default html tag and automatically wraps any signals in watch().`ts
import {LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {SignalWatcher, html, signal} from '@lit-labs/signals';const count = signal(0);
@customElement('signal-example')
export class SignalExample extends SignalWatcher(LitElement) {
static styles = css
; render() {
return html
The count is ${count}
;
} #onClick() {
count.set(count.get() + 1);
}
}
`####
withWatch()withWatch() is a function that wraps an html tag function with the
auto-watching functionality. The html tag exported by @lit-labs/signals is a
convenient export of the core lit-html template tag wrapped with withWatch().withWatch() allows you to compose the signal watching wrapper with other
lit-html tag wrappers like Lit's withStatic() utility.`ts
import {html as coreHtml} from 'lit';
import {withStatic} from 'lit/static-html.js';
import {withWatch} from '@lit-labs/signals';/**
* A Lit template tag that support static values and pinpoint signal updates.
*/
const html = withWatch(withStatic(coreHtml));
`Future Work
This library will change based on feedback from developers. Some existing dieas we have for futher development are:
- A signal-aware
repeat() directive that can update items in a list independently of the entire list.
- Signal aware when() directive that wraps the condition in a computed signal and watches it.
- A @property() decorator that creates a signal-backed property that can be watched.
- An @effect() method decorator that runs a method inside a watched computed signal, and re-runs it when any signal dependencies change. This would be an alternative the the common @observe() feature request.
- Batched synchronous updates, when using a utility like []batchedEffect()](https://github.com/proposal-signals/signal-utils?tab=readme-ov-file#batched-effects)Related Libraries
$3
signal-utils` projectSome of these are especially useful for use cases around shared observable state. The signal-backed collections (arrays, maps, and sets) can help address cases where Lit's reactive properties cannot see internal changes to objects.