Open mbeckem opened 2 months ago
Thanks for the detailed writeup.
Wrapping things in a computed does not cover the use-cases we have in the react and preact adapters, as the start and end of the tracking context are not implemented via the JS stack. Doing so would require wrapping React components, which is a huge can of worms - you have to copy properties onto the wrapper function, it breaks classes, changes stack traces in errors, adds overhead to every component, etc. Having the ability to start and stop a completely custom tracking context is lower-level and accommodates all synchronous use-cases.
import {effect} from '@preact/signals-core';
function watcher(onNotify: () => void): () => () => void {
let start;
effect(function () {
this.N = onNotify;
start = this.S.bind(this);
});
return start;
}
// usage:
const start = watcher(() => console.log('accessed signal changed'));
const end = start();
try {
// access some signals here
} finally {
end();
}
Hi everyone,
first of all thank you for developing
@preact/signals-core
. Your library has been extremely helpful to manage state across multiple UI frameworks.I'd like to propose a feature that would make integration with other frameworks even better.
TLDR: Add a way to subscribe to signals without triggering the re-computation of computed signals.
There are currently two ways to observe changes of a signal's value: using
effect()
directly or callingsignal.subscribe(...)
, which is implemented in terms ofeffect
. Both ways will accesssignal.value
, even if we don't actually need it. This is not a problem for "normal" signals (the access is cheap since the value is already available), but it may be a problem for computed signals:Proposed API
I'd like to propose a new method on the
Signal
class, analogous tosubscribe
:The appropriate place to invoke these callbacks appears to be the existing
Signal._notify()
method.Use cases
Many APIs are based on reading values at specific times (often controlled by the framework) and events that tell the framework that a previously read value has become outdated. I'm using React's
useSyncExternalStore
hook for this example, but the principle applies to other APIs, too.With a computed signal:
In a typical UI, the
Component
above will only render (at most) ~60 times a second. However, ifmySignal
's dependencies change often, theeffect
will also trigger as often, needlessly recomputingmySignal.value
. Error handling also suffers a bit in my opinion: ifmySignal.value
throws in theeffect
, it is not obvious where to report the error. But throwing fromgetSnapshot
would usually be acceptable for error handling.With the proposed API:
Note that there are ways to work around this problem without changing the code of the library. However, all my attempts have made reading and subscribing very complicated / brittle, which kinda defeats the purpose.
I sometime wish to create a "tracked context" (i.e. recording signal accesses) without scheduling automatic executions like
effect()
does. I just want to be notified when any of the signals that were used has changed. Your react runtime integration seems to do the same thing, using the internal methods ofEffect
. I realized that this is just a special case of the use case above, and listening to signal invalidations with the proposed API could make this approach less brittle:signal.onInvalidate()
. When a change happens, simply rerender the component.Additional thoughts / alternatives
Instead of
onInvalidate
becoming a method, it could also be an option during signal creation. Other signal options have already been proposed, for example in #351.This can also be implemented by calling internal
Signal
methods (or patching them slightly), which requires using the minified names of those methods. I find that very brittle, and I believe this deserves to be supported natively in some way.The signals proposal uses a
Watcher
class to achieve a similar thing. One or more signals can be watched, and any change will trigger anotify
callback execution. without explicitly computing the new value. API copied for reference:The signals proposal also sketches an implementation of
effect
based on computed signals +Watcher
that I found interesting. This technique could be used to (mostly) unify the implementationEffect
andComputed
..