Replies: 1 comment 1 reply
-
@swarmukhdefault This is a really great question, and it's something I don't believe we ended up covering in the documentation or release post(s). The TLDR is: we intentionally kept Signals the lowest-level primitive because it was the only way to preserve JS semantics, and it still allows complex data structures to be built on top. The analogy for me is variable references in JavaScript VS complex types. With the exception of implied globals, changes (assignments) to JS variables cannot be observed, whereas changes to complex types can (via Proxy, setters or dirty checking): let a = 1;
a = 2; // no language mechanism for observing this, so we had to create one
let b = {x:1};
b.x = 2; // already 3 language mechanisms for observing this, so we avoided adding one I'll break down the key reasons why we ended up not implementing complex type reactivity, and instead stuck with referential reactivity for Signals. There are a few other reasons (like library size and performance), but these were the most fundamental ones. Breaks automatic unwrappingThe first reason we went with this simple approach is that it is the only way to support automatic unwrapping of Signals when referenced via JSX. We need to be able to go from a reference to its value, which only works when references and values are one-to-one: const count = signal(0);
count.value = 1;
<span>count is {count}</span>;
// ^^^^^ the renderer knows the value is count.value
// now consider if we supported object signals:
const state = signal({ count: 0 });
state.count = 1;
<span>count is {state.count}</span>;
// ^^^^^^^^^^^ we're only passing the value now - the renderer can't see the signal Now, the above can be addressed by having // today:
const state = signal(new Set('a'));
state.value.has('a'); // true
state.value.add('a');
console.log([...state.value]); // ['a']
// if we did deep reactivity:
const state = signal(new Set('a'));
state.value.has('a'); // false - the item at #0 was converted to Signal('A')
state.value.add('a');
console.log([...state.value]); // [signal('a'), signal('a')] Generalization IssuesThe second reason Signals works on simple referential equality is because the semantics for equality within complex types are not generalizable. As an example, imagine a Signal that contains a WeakSet or WeakMap - since it's not possible to enumerate their keys, it would be impossible to implement update/change detection for Proxy complexity/breakage/performanceFinally, if Signals were to be aware of the Array, Map and Set data types, they would also need to be capable of the same tracking for Object. Object can't be reliably tracked via the simple setter mechanism Signals uses, because Objects are not sealed - the only reliable mechanism for tracking Object mutations is to use a Proxy, which is relatively expensive. Automatic proxying also alters the expected semantics of Objects in ways that may be unexpected - setting a property on a Signal-ified Object would trigger any associated computeds and effects, which would in turn alter the object mid-property-access. These issues exist in the current version of Signals, but because we limited reactivity to simple singular references, their impact on JS semantics is negligible. That all said, I really do want to see folks build full Reactive Object implementations on top of Signals. The original version of Signals (called https://gist.github.com/developit/19163156cbea17760c11d597ae692a57 A few projects have already popped up that straddle this, like deepsignal. |
Beta Was this translation helpful? Give feedback.
-
Data change for composite objects (i.e.
Array
s,Map
s,Set
s, etc.) in UI frameworks/libraries (one out of many exceptions: Angular) is generally represented as new reference object with the data inserted into or removed from this new reference, a typical e.g.setState(oldState => ([...oldState, newState]))
.Taking
signal
s for a spin, I could implement data update forArray
s,Set
s andMap
s in a much more unobtrusive/natural way as is normally used (or at least how they are meant to be used in the first place), i.e. in-place mutation, and the UI still reflects the latest/updated content. This is charming!Keeping aside the pros and cons of immutability for a moment, with
signal
s I see that the same style of data update is demonstrated in the examples and documentations. While I do understand the purpose/rationale of such update mechanism, can we not, or better still, embrace Web APIs as is and work in tandem with the update mechanisms as laid out for such composite objects, given that now (at least as mentioned in the docs) thesignal
reference is the same?The primary reason for such a discussion being that overtime, usage of
signal
will increase, and as people from different backgrounds embrace the beauty of it, varied update mechanisms will also come into play (one being usage of Web API as is).Beta Was this translation helpful? Give feedback.
All reactions