Open EisenbergEffect opened 7 months ago
Lit was able to integrate the proposal with very little effort and code.
Tweet here:
https://twitter.com/justinfagnani/status/1774880922560815222
Demo here:
https://lit.dev/playground/#gist=a1b12ce26246d3562dda13718b59926c
So I did hit a problem with trying to port the watch()
directive from our @lit-labs/preact-signals
package.
The watch()
directive takes a signal, binds it to a specific DOM location and synchronously updates the DOM when the signal changes. The watch()
directive also reads the signal outside of tracking (using Preact's signal.peek()
and signal.subscribe()
) in order to not trigger an outer computed signal that's watching the entire render method of a component. This let's watch()
be used for targeted DOM updates while a computation wrapping the render can batch and react to any signal access, but not re-render a signal that's only passed to watch()
.
This was my attempt at a port:
override render(signal: Signal.State<any> | Signal.Computed<any>) {
if (signal !== this.__signal) {
this.__dispose?.();
this.__signal = signal;
// Whether the Watcher callback is called because of this render
// pass, or because of a separate signal update.
let updateFromLit = true;
const watcher = new Signal.subtle.Watcher(() => {
if (updateFromLit === false) {
this.setValue(Signal.subtle.untrack(() => signal.get()));
}
});
watcher.watch(signal);
this.__dispose = () => watcher.unwatch(signal);
updateFromLit = false;
}
// We use untrack() so that the signal access is not tracked by the watcher
// created by SignalWatcher. This means that an can use both SignalWatcher
// and watch() and a signal update won't trigger a full element update if
// it's only passed to watch() and not otherwise accessed by the element.
return Signal.subtle.untrack(() => signal.get());
}
High level experience report from @yyx990803 : https://twitter.com/youyuxi/status/1776560691572535341
I was involved quite early in the proposal’s discussions. Overall I think it’s nice to have a built-in primitive so that frameworks can ship less code and have better performance and potential interop (think cross-framework VueUse). We are currently prototyping to make sure Vue reactivity can be re-implemented on top of it.
I made a little drum machine demo here: https://github.com/dakom/drum-machine-js-signals
(hat tip @EisenbergEffect who pointed out that this is a good place to share it)
Another demo here: https://github.com/dakom/local-chat-js-signals
This one tests out the idea of doing efficient non-lossy list updates, by having 4 different "chat" windows with edit, delete, etc.
The "chat" is just local, nothing is sent out over the internet
Similar to the drum machine above - no framework, just vanilla
Integration with @ibyar/aurora
import { Component } from '@ibyar/aurora';
import { Signal } from 'signal-polyfill';
@Component({
selector: 'signal-proposal',
template: `<div>
<button class="btn btn-link" (click)="resetCounter()">Reset</button>
<p>{{counter?.get()}}</p>
<button class="btn btn-link" (click)="addCounter(+ 1)">+</button>
<button class="btn btn-link" (click)="addCounter(- 1)">-</button>
<input type="number" [value]="+counter?.get() ?? 100" (input)="counter?.set(+$event.target.value)"/>
</div>`
})
export class SignalProposal {
counter?: Signal.State<number> | null = new Signal.State(100);
resetCounter() {
this.counter = new Signal.State(100);
}
addCounter(num: number) {
this.counter?.set(this.counter?.get() + num);
}
}
requird to init scope: https://github.com/muhammad-salem/aurora/blob/feature/signal-proposal/src/core/signals/proposal-signals.ts
Integration with view https://github.com/muhammad-salem/aurora/blob/feature/signal-proposal/src/core/view/base-view.ts#L72
Here's my attempt at making Solid.js' signals on top of the spec (just the signals and effects, no suspense and transitions)
https://gist.github.com/mary-ext/2bfb49da129f40d0ba92a1a85abd9e26
I've only tested running it standalone at the moment, but it'll probably work just as well once I've gotten dom-exressions
to work with it (the actual DOM manipulation "runtime" that Solid.js uses)
I've some gripes with making batched synchronous reactions happen with the watchers, right now they're preventing me from just having one watcher for all effects.
But other than that, everything else has been great. I can somewhat understand why synchronous reactions aren't allowed especially when it's being done naively, but I'm also somewhat unsure of it at the moment.
I have developed a tsx
application framework called airx
that fully embraces Signal
. It is designed for learning purposes. Here is a code snippet:
const count = new Signal.State(0);
function Counter() {
const handleClick = () => {
count.set(count.get() + 1);
};
return () => (
<button onClick={handleClick}>
count is {count.get()}
</button>
);
}
Quick demo: https://codesandbox.io/p/devbox/github/airxjs/vite-template/tree/signal/?file=%2Fsrc%2FApp.tsx%3A7%2C1
A list of library/framework integration experiences to help validate the proposal.