Re: [WICG/webcomponents] Idea: A tree-aware task scheduler (Issue #1055)

I think that might be a different problem, basically relating only to the creation order of component trees. Your example @titoBouzout might not consider what happens with updates over time, and I *think* that @justinfagnani is alluding to is that something like this psuedo code,

```jsx
<div id="1"></div>

queueDOMUpdate(function Component(shouldShowSignal){
 return shouldShowSignal() ? null : <div id="2"></div>
})

<div id="3"></div>
```

would actually be more like the following where an update mechanism (f.e. an effect, or `update` method in Lit, or function component re-run in React, etc) would be scheduled in a way such that the ordering of reactions (effects, update calls, component re-runs, etc) happen in tree order:

```jsx
<div id="1"></div>

<placeholder id="placeholder" />

function Component(shouldShowSignal) {
  createDOMUpdateEffect(() => { // using Solid.js terminology here
    placeholder.innerHTML = "" // clear
    placeholder.append(shouldShowSignal() ? null : <div id="2"></div>)
  })
})

<div id="3"></div>
```

The main idea here is that, something like `createDOMUpdateEffect` could be basically an effect just like in Solid, but the scheduling of it happens that it is executed in tree-order relative to any other such effects (as opposed to simply added to a queue with `queueMicrotask`).

And the reason Justin wants this is so that if

- child component/element property is updated
- parent component/element property is updated

then the effects will run within the next microtask in the order of

- the createDOMUpdateEffect in the parent
- the createDOMUpdateEffect in the child

instead of

- the createDOMUpdateEffect in the child
- the createDOMUpdateEffect in the parent

Basically the tree order determining the order of reactions (imagine the same thing but scheduling `update()` calls in Lit instead of effects Solid or Lume Element, etc).

The result would be that, no matter what, if both parent and child were modified, and for example the parent will remove the child due to its state change, the effect for the child could be cleaned up and never run (whereas if it ran first, it would be wasteful).

While I understand this desire, I feel like this is an edge case that is much too specific to DOM and certain ways of implementing custom elements (or components), and is completely unnecessary to block shipping effects for sake of handling one specific way of writing DOM code.

Microtask effects will not be in tree order, and they will serve many people well. 

---

In my custom elements, I can handle this use case easily, even with sycnrhonous-non-microtask effects:

```js
connectedCallback() {
  super.connectedCallback()
  this.createEffect(() => {
    someValue()
    const raf = requestAnimationFrame(() => {
      // ...heavy update with someValue...
    })
    onCleanup(() => cancelAnimationFrame(raf)) // cleanup runs on disconnect, not just before any re-run triggered by someValue()
  })
}
```

where `this.createEffect` is a wrapper around Solid.js `createEffect` with the added convenience that it cleans up on `disconnectedCallback` automatically. If I have expensive rendering logic that should be batched, I can easily do that by batching it into a requestAnimationFrame, *even if the effect is synchronous* which in Solid.js 1.0 it historically has been (though microtask effects would be much more ideal).

With this pattern, I can easily, for example, immediately remove children, and the next requestAnimationFrame won't have expensive logic for the removed child (even if the child's data changed before the parent).

All I'm saying is, I don't think we need this complexity to ship some sort of basic effect that will be super useful to a lot of people. This specific type of scheduling desire is valid, but it is very specific, not generic.

Furthermore, something like `queueDOMMicrotask` *can easily be combined* with a basic microtask-effect to achieve the same purpose. In this example, I'll use a hypoithetical API names `createMicrotaskEffect` to denote that it is a simple effect API that simply queues a single reaction in the next microtask, but it could just as easily be replaced with a `createSynchronousEffect` that runs immediate on signal change:

```js
let queued = false

createMicrotaskEffect(() => {
  // dependencies
  signalOne()
  signalTwo()

  if (queued) return
  queued = true
  this.queueDOMUpdate(() => { // `this` is a custom element, for example
    queued = false

    // run tree-order logic here (runs in the next microtask, for example)
  })
})
```

And just like that, by pairing both APIs together, we've solved some problems, without requiring effects have special types of scheduling. Replace `createMicrotaskEffect` with `createSynchronousEffect` and it works just the same.

What would be useful is to make the effect API flexible enough to easily be able to create APIs like `createDOMUpdateEffect` out of simpler effects like `createMicrotaskEffect`.

Basically, we don't need to block effects on anything like this. And yeah, *maybe* `queueDOMUpdate` could be useful (I've been fine writing intensive WebGL apps (where modifying reactive properties can recreate a whole geometry and shader to expensively upload both to the GPU again) using declarative templating within custom elements, and I've been fine without this type of scheduling, although it *might* be convenient in a few cases but most of the time I wouldn't need it). 

-- 
Reply to this email directly or view it on GitHub:
https://github.com/WICG/webcomponents/issues/1055#issuecomment-2081647451
You are receiving this because you are subscribed to this thread.

Message ID: <WICG/webcomponents/issues/1055/2081647451@github.com>

Received on Sunday, 28 April 2024 20:41:02 UTC