0kku / destiny

A reactive UI library for JavaScript and TypeScript
Open Software License 3.0
53 stars 6 forks source link

Investigate easing restrictions on computed() #50

Open 0kku opened 1 year ago

0kku commented 1 year ago

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:

const elements = computedArray(() => Object.keys(someRV.value))
  .map(key => html`
    <p class=${someOtherRV}>${key}</p>
  `);

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:

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.