tc39 / proposal-signals

A proposal to add signals to JavaScript.
MIT License
2.87k stars 54 forks source link

Scopes #187

Open doeixd opened 3 weeks ago

doeixd commented 3 weeks ago

Right now, all the signals share the same hidden state and are apart of the same reactivity system.

I was wondering why this shouldn't be made explicit, to control the hidden state, and group together sub-graphs #159 , to possibly allow for different reactive algorithms, bulk deallocation/garbage collection, better dev tools, and labeling of the reactive graph, or maybe things like Relays?

I'm not a JS expert. But maybe this could look like:

// The previously hidden global state and reactive runtime, will be moved into the scope.
const scope = new Scope({
  algorithm: 'default',
  name: 'default',
  dispose: () => {}
})

// There could be a default global scope to make the api more ergonomic.
const value = new Signal('value', { scope })

// You can view the scope here = value[Signal.subtle].scope 

It's just an idea. Let me know what you think.

Prior Art OCaml Incremental Scopes Leptos Runtimes SolidJS Root

shaylew commented 3 weeks ago

This is definitely worth considering!

It would be a bit more flexible and less magical to be able to have multiple signal graphs rather than one global state. On the other hand, it would seriously harm any hope of interop for frameworks to be using different signal graphs without shared autotracking, so this capability could easily turn into a footgun. If we did have multiple graphs, we might need to come up with a compelling story for how you're supposed to intentionally bridge between two signal graphs if you want to use them in concert. (That's not necessarily a bad thing -- I think system boundaries like Watcher are where the current proposal is weakest, and having the boundary APIs be up to the task of stitching two separate signal graphs together seamlessly after the fact would be proof of a sort that the boundary APIs were sufficiently expressive.)

I think in Incremental the corresponding concept would be the Incremental.Make generative functor, which mints an entirely new Incremental module with distinct types -- scopes are a different thing, and multiple scopes do live within the same dependency graph. I hadn't seen Leptos's runtimes before but it looks like more or less the same idea as Incremental's, just encoded via CPS for lack of existential types.

Solid's idea of multiple roots isn't really in the same space; Solid keeps only one global state, and any source you read (regardless of what root it was created in) can be autotracked by any memo or effect.

dead-claudia commented 2 weeks ago

I didn't see this when I made the comment, but I just realized the long framework hypothetical in the third paragraph in https://github.com/tc39/proposal-signals/issues/195#issuecomment-2083895142 is alerting to encapsulation issues in the current design that (in part) requires much of this very thing to solve.

I approached it from a different angle, but ultimately, it boils down to a need for scoping, just for consistency across watcher composition.

guitarino commented 2 weeks ago

We have a need for something like scopes, and I'm not sure how we'd be able to implement it without them

We have a scenario where we an application uses signals for its internal state, as well as exposes some of them to an external user. However, it's important for the application to control the timing of when signals get updated - it is a 3D application that needs to update its internal state during a frame, and only allow the external user to react to updates after the frame update

The way I would do it with scopes is:

MobX has the ability to isolate global state, so this feature can also be important in order for the existing libraries to start using the native signals internally

Additionally, due to the API's ability to return signals contained in watchers and computeds via introspectSources, it is somewhat of a security concern that signal scopes cannot be isolated from each other. If a malicious library got an access to a function that reads certain signals, by calling that function inside a watcher, they'd be able to find out the contents of those signals. With scopes, the users would be able to safeguard their signals against watchers and computeds that don't share the same scope