tc39 / proposal-signals

A proposal to add signals to JavaScript.
MIT License
3.1k stars 55 forks source link

Integration Stories #116

Open EisenbergEffect opened 3 months ago

EisenbergEffect commented 3 months ago

A list of library/framework integration experiences to help validate the proposal.

EisenbergEffect commented 3 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

justinfagnani commented 3 months ago

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().

See: https://github.com/lit/lit/blob/23121c203965d4c74f78bebfc1d6bcc79e5b6738/packages/labs/preact-signals/src/lib/watch.ts#L23

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());
  }
littledan commented 3 months ago

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.

dakom commented 2 months ago

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)

dakom commented 2 months ago

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

muhammad-salem commented 2 months ago

Integration with @ibyar/aurora

example: https://github.com/muhammad-salem/aurora/blob/feature/signal-proposal/example/src/signals/proposal.ts

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

mary-ext commented 1 month ago

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.

  1. It's not telling me who specifically got marked dirty at the time of callback
  2. I'd have to unmark the watcher every time the notify callback is fired

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.

yinxulai commented 1 month ago

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