proposal-signals / signal-utils

MIT License
69 stars 6 forks source link

Add reaction() utility #61

Closed justinfagnani closed 1 month ago

justinfagnani commented 2 months ago

This adds a reaction() utility that's similar to MobX's reaction() as mentioned in https://github.com/proposal-signals/signal-utils/issues/15#issuecomment-2062018823.

reaction() takes two arguments, a data function and an effect function. The data function is wrapped in a Computed, and when the return value of the data function changes, the effect function is called with the current and previous values. An optional equality function is accepted.

const x = new Signal.State(0);
reaction(() => x.get(), (value, previousValue) => {
  console.log('x changed', value, previousValue);
});
x.set(1);
await 0;
// x changed 0 1

The effect function is only called the first time the data function return value changes. It is not called with the initial value.

Reactions can be unsubscribed to by invoking the cleanup function that it returns.

NesCafe62 commented 2 months ago

hello @justinfagnani I'm trying to understand the code from this reaction utility. and comparing it with effect implementation. two questions come to mind:

  1. is it ok that effect uses single global watcher, but reaction creates own instance of watcher on every reaction?
  2. inside reaction effect(value, prevValue) is called synchronously (comparing to effect that uses queueMicrotask). should reaction also use queueMicrotask?

p.s. usually I imagined reaction as an effect with untrack

function reaction(getter, reactionFn) {
   effect(() => {
      const value = getter();
      untrack(() => reactionFn(value));
   });
}

but with hope it is possible for more optimized way (without auto-collect dependency on each call), so watcher is the way to allow that

justinfagnani commented 1 month ago

@NesCafe62

is it ok that effect uses single global watcher, but reaction creates own instance of watcher on every reaction?

I guess? effect() is batching all effect callbacks in a single microtask, reaction() will use a separate microtask for each callback. Microtasks are very cheap these days, so I'm not concerned about the cost of them for this use case. I regularly use fast apps that enqueue thousands of microtasks per render.

inside reaction effect(value, prevValue) is called synchronously (comparing to effect that uses queueMicrotask). should reaction also use queueMicrotask?

reaction() is enquing a microtask with this like:

await 0;

Awaiting anything enqueues a microtask. This is similar to await Promise.resolve() or queueMicrotask(...) but without the Promise allocation, or function call + closure allocation. And it's short. I added a comment.