You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 calling signal.subscribe(...), which is implemented in terms of effect. Both ways will access signal.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:
the computation may be expensive (degrading performance)
the computation may have side effects or may throw exceptions
Proposed API
I'd like to propose a new method on the Signal class, analogous to subscribe:
classSignal<T>{// Watches the signal and invokes `fn` when the signal's previous value is no longer valid.// For plain signals, `fn` will be called when the signals's value has been set.// For computed signals, `fn` will be called when a dependency has changed (i.e. when// reading `signal.value` _would_ trigger a recomputation).onInvalidate(fn: ()=>void): ()=>void;}
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.
functionComponent(){// getSnapshot must return the current value, usually with caching.// subscribe shall be called whenever that value changes; this effectively triggers// a rerender and as a consequence another call to `getSnapshot`.constcurrentValue=useSyncExternalStore(subscribe,getSnapshot);}
With a computed signal:
constmySignal=computed(()=>/* non-trivial code */);functionComponent(){constgetSnapshot=useCallback(// We can just read the value here, and get automatic caching. Great!()=>mySignal.value,[]);constsubscribe=useCallback((cb)=>{// We must read `.value` here to become notified about changes.// We will be notified correctly, but we can end up recomputing `.value` very often - without even using it!returneffect(()=>{mySignal.value;untracked(cb);});},[]);constcurrentValue=useSyncExternalStore(subscribe,getSnapshot)}
In a typical UI, the Component above will only render (at most) ~60 times a second.
However, if mySignal's dependencies change often, the effect will also trigger as often, needlessly recomputing mySignal.value.
Error handling also suffers a bit in my opinion: if mySignal.value throws in the effect, it is not obvious where to report the error. But throwing from getSnapshot would usually be acceptable for error handling.
With the proposed API:
constmySignal=computed(()=>/* non-trivial code */);functionComponent(){constgetSnapshot=useCallback(()=>mySignal.value,[]);constsubscribe=useCallback((cb)=>{// As cheap as possible. Nice!returnmySignal.onInvalidate(cb);},[]);constcurrentValue=useSyncExternalStore(subscribe,getSnapshot)
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 of Effect.
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:
Wrap the code of the "tracked context" (e.g. a React component) into a computed signal.
The signal tracks the dependencies for us.
(If one does not want the automatic caching, an artificial dependency can be used to invalide the signal).
Listen for changes via signal.onInvalidate(). When a change happens, simply rerender the component.
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 a notify callback execution. without explicitly computing the new value. API copied for reference:
classWatcher{// When a (recursive) source of Watcher is written to, call this callback,// if it hasn't already been called since the last `watch` call.// No signals may be read or written during the notify.constructor(notify: (this: Watcher)=>void);// Add these signals to the Watcher's set, and set the watcher to run its// notify callback next time any signal in the set (or one of its dependencies) changes.// Can be called with no arguments just to reset the "notified" state, so that// the notify callback will be invoked again.watch(...s: Signal[]): void;// Remove these signals from the watched set (e.g., for an effect which is disposed)unwatch(...s: Signal[]): void;// Returns the set of sources in the Watcher's set which are still dirty, or is a computed signal// with a source which is dirty or pending and hasn't yet been re-evaluatedgetPending(): Signal[];}
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 implementation Effect and Computed..
The text was updated successfully, but these errors were encountered:
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';functionwatcher(onNotify: ()=>void): ()=>()=>void{letstart;effect(function(){this.N=onNotify;start=this.S.bind(this);});returnstart;}// usage:conststart=watcher(()=>console.log('accessed signal changed'));constend=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, if
mySignal
's dependencies change often, theeffect
will also trigger as often, needlessly recomputingmySignal.value
.Error handling also suffers a bit in my opinion: if
mySignal.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 of
Effect
.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:
The signal tracks the dependencies for us.
(If one does not want the automatic caching, an artificial dependency can be used to invalide the signal).
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 Subscription integration for third-party libraries #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 a
notify
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 implementation
Effect
andComputed
..The text was updated successfully, but these errors were encountered: