Currently, creating new reactive values inside the callback of computed() (and similar methods) is disallowed because accessing the value of a newly created reactive entity inside the computation would add that value to the dependencies of the computed value, creating a leak that piles on exponentially on every update.
The following example that fails was given:
const someRV = new ReactiveValue({foo: 1, bar: 2});
cosnt someOtherRV = new ReactiveValue("qux");
const elements = computed(() =>
Object.keys(someRV.value)
.map(key => html`
<p class=${someOtherRV}>${key}</p>
`)
);
The expected behaviour is, that the elements array gets recomputed whenever someRV changes. However, computed() can't tell the difference between someRV and someOtherRV because the template synchronously accesses the value of someOtherRV in order to build the document fragment, thus adding it to the dependencies of the computed value alongside someRV. Thus, the entire array would be reconstructed whenever either of the two values change. While this works and is allowed, it unexpectedly does more work than intended and necessary, without warning you. Now, if one was to map someOtherRV into a new reactive value, or use a computed value to create a new reactive value inside the template, this would throw, stating that creation of new reactive values inside a computed value is disallowed.
The correct way to write the above code would be moving the mapping outside the computation:
This would work with the mapped or computed values inside the template too, since it would no longer be inside the computation. However, the question is, should the first example just work? Is making it work worth the added complexity?
Conceivably, this use-case could be supported by doing all of the following:
Making computations (computed(), computedArray(), sideEffect(), html templates) into microtasks, deferring their evaluation until the previous computation is complete, thus allowing nested computations without one interfering with another.
Allowing creation of reactive entities inside computed values by performing the following steps:
Make an AbortSignal available when a computation starts
Have any reactive entities that are created during the computation make a note of the currently running computation and its abort signal
Have any reactive entities that were created refrain from adding themselves as a dependency to the currently running computation
Have the computation abort the signal once the computation is done
In response to the signal being aborted, have any reactive entities that were created remove the note about the computation, releasing it for use in future computations.
It's not clear if supporting the first pattern is worth the added complexity, considering that that specific use-case is enabled by changing the code to the second example. The code in the latter example is more idiomatic to the programming paradigm in question anyway, so I'm not sure if encouraging the former is wise. On the other hand, one might argue that it's surprising and unintuitive that it doesn't "just work", since in non-reactive code there wouldn't be any reason it would matter which order you do things in. Furthermore, perhaps there are additional use-cases I haven't thought of that can't so easily be refactored to side-step the issue? More feedback may be necessary.
Currently, creating new reactive values inside the callback of
computed()
(and similar methods) is disallowed because accessing the value of a newly created reactive entity inside the computation would add that value to the dependencies of the computed value, creating a leak that piles on exponentially on every update.The following example that fails was given:
The expected behaviour is, that the
elements
array gets recomputed wheneversomeRV
changes. However,computed()
can't tell the difference betweensomeRV
andsomeOtherRV
because the template synchronously accesses the value ofsomeOtherRV
in order to build the document fragment, thus adding it to the dependencies of the computed value alongsidesomeRV
. Thus, the entire array would be reconstructed whenever either of the two values change. While this works and is allowed, it unexpectedly does more work than intended and necessary, without warning you. Now, if one was to mapsomeOtherRV
into a new reactive value, or use a computed value to create a new reactive value inside the template, this would throw, stating that creation of new reactive values inside a computed value is disallowed.The correct way to write the above code would be moving the mapping outside the computation:
This would work with the mapped or computed values inside the template too, since it would no longer be inside the computation. However, the question is, should the first example just work? Is making it work worth the added complexity?
Conceivably, this use-case could be supported by doing all of the following:
computed()
,computedArray()
,sideEffect()
,html
templates) into microtasks, deferring their evaluation until the previous computation is complete, thus allowing nested computations without one interfering with another.It's not clear if supporting the first pattern is worth the added complexity, considering that that specific use-case is enabled by changing the code to the second example. The code in the latter example is more idiomatic to the programming paradigm in question anyway, so I'm not sure if encouraging the former is wise. On the other hand, one might argue that it's surprising and unintuitive that it doesn't "just work", since in non-reactive code there wouldn't be any reason it would matter which order you do things in. Furthermore, perhaps there are additional use-cases I haven't thought of that can't so easily be refactored to side-step the issue? More feedback may be necessary.