owl icon indicating copy to clipboard operation
owl copied to clipboard

Toward Owl 3.0

Open ged-odoo opened this issue 10 months ago • 5 comments

Toward Owl 3.0 [DRAFT!!!!!!]

Motivation

After a few years of use of Owl 2 at Odoo, I believe we have a reasonable understanding of the strengths and weaknesses of owl.

Owl has many great qualities, we definitely want to keep that. But it can still be improved. This document is trying to open the discussion on what owl 3.0 could look like. Of course, at this point, it is only a discussion. Nothing concrete yet.

Main ideas/themes

Here are a few research/ideas that came up in various discussions.

  • improve various APIs (simpler, more intuitive, more explicit)
    • xml`` should return the function
    • no need for a canonical root => everything in an owl App is a Root
    • useRef is awkward
  • improve the semantics of some parts of the templating
    • lazy t-set body
    • scoping semantics
    • force using this to get the component in template expressions
    • t-call with explicit arguments
    • ... ?
  • better primitives (lazy blocks instead of slots)
    • life cycle on blocks? (maybe t-on-mounted="this.onMounted")
  • better perfs
    • some of the change above should help
    • a lazy block (or a slot) should be able to create an "effect" and be updated independently
  • revamp the reactivity system
    • probably something like effects/signals (uncouple subscription/updates)
    • allow a way to define derived reactive values
    • also, derived ASYNC reactive values, with control on restarting/stopping/... computations
  • make the rendering engine synchronous
    • remove onWillStart, onWillUpdateProps
    • move asynchrony into reactivity system
  • try to simplify owl
    • remove support for deep render, remove this.render method
    • remove t-att syntax with pairs

Many of these ideas have already issues (see https://github.com/odoo/owl/issues?q=is%3Aissue%20state%3Aopen%20label%3Aowl-3)

Async Reactive Engine

Big change. Basically, move the fiber async code out of the rendering engine and into the reactivity system. The goal is to give access to the strong concurrency code from owl to other code (the model, in addition to the view). At the same time, it feels like rendering a component (or a lazy block) should be an effect, and all reactive values should be automatically subscribed to that effect (so, basically, effects/signals)

Synchronous Rendering

If owl rendering is synchronous, the question is what happens when a component want to fetch something asynchronous before it is rendering?

Currently, in owl 2.0, it looks like this:

class MyComponent extends Component {
    static template = xml`<div><t t-esc="value"/></div>`

    setup() {
        onWillStart(async () => {
            this.value = await loadSomeValue();
        });
    }
}

If the rendering is synchronous, how can we do the same? An easy way is to move the loading of the data up the component tree. It works, but it loses some composability/ease of working with owl.

Another way would be to use async derived values:

class MyComponent extends Component {
    static template = xml`<div><t t-esc="value"/></div>`

    setup() {
        this.value = useAsyncState(() => loadSomeValues());
    }
}

Owl could then basically treat the component as an empty text node until all async values are ready, and then rerender it.

But what happens if our fetch operation is long, and in the meantime, we want to change some parameters? Currently, if it happens, owl has to destroy the component and restart again. But i guess we could either do that, or just recompute the async derived value.

class MyComponent extends Component {
    static template = xml`<div><t t-esc="value"/></div>`

    setup() {
        this.value = useAsyncState(() => loadSomeValues(this.props.recordId));
    }
}

Lazy block

Lazy block could replace slots, and named slots, but explicitely.

<t t-set-block="coucou">
    <div>this is some lazy content <t t-esc="this.someValue"/></div>
</t>
<SomeComponent content="coucou"/>

and in SomeComponent:

<!--  only evaluate its content now-->
<t t-call-block="coucou" /> 

Maybe we want to keep a simple syntax for the common case of default slot:

<SomeComponent content="?">
    <div>this is some lazy content <t t-esc="this.someValue"/></div>
</SomeComponent>

How to reference the inner content? Maybe this?

<SomeComponent t-set-block="coucou" content="coucou">
    <div>this is some lazy content <t t-esc="this.someValue"/></div>
</SomeComponent>

Incremental transition?

All changes discussed above should be done with the practical transition work in mind. So, since there are obviously breaking changes, and upgrading from owl 2 to 3 will need some work, we should have a "easy" way to proceed.

  • ideally, it should allow an incremental upgrade. For example, allowing a component written in owl 2 to work in owl 3 until it has been converted
  • maybe templates/components should be explicitely flagged as owl 2 or 3, to trigger the owl 2 or 3 compiler?

ged-odoo avatar Feb 21 '25 13:02 ged-odoo

A few things that you missed or aren't currently tracked in any issues:

  • destructuring assignment (mostly for t-for(each), ideally should also be supported in t-set or other assignments I might be forgetting)
  • A better (and standard) way for components to expose an imperative API to their parent, could possibly be solved by a better ref API similar to react's useImperativeHandle.
  • This is kind of technical but it would be nice if we could use scopes instead of context objects for variables in compiled templates as it would likely generate better errors and make templates even more readable, not sure this is feasible though.
  • Keeping error handling and emission in mind while designing the new version
  • Suspense? In general I think that data-fetching needs a good amount of forethought. It would definitely be good if we somehow could get a tree of the data dependencies without having the component tree. I think a lot of research into the current state-of-the art is warranted (one library worth looking at: tanstack query)

I like the idea of tagging templates to choose the compiler as that's pretty easy to keep separate, not sure how viable it will be to have a common runtime for both compilers though but definitely worth exploring. Might also be good if the new compiler could warn (or error) on old constructs to help with conversions.

One big question that remains to be discussed and decided upon is, with a rework of the reactivity system it could be feasible to push granularity even further (eg each t-att- generates a separate render effect), similar to what's done in Solid for example. Not sure what the trade-offs are.

Something to keep in mind for the signal discussion: https://github.com/tc39/proposal-signals While this is still likely some years away (if it ever sees the light of day), making a basic effort to keep our APIs compatible in some sense with the proposal and/or with other implementations could allow us to transition to standard signals more seamlessly if that ever happens.

sdegueldre avatar Feb 24 '25 14:02 sdegueldre

i believe that solid actually group some signals together, so it is not at the absolute maximal granularity. for owl, my first intuition would be to explore the granularity of a "block", in the sense of blockdom. but this is just a thought, no real work done yet

ged-odoo avatar Feb 24 '25 15:02 ged-odoo

One more thing to add to the list of things to evaluate: can we (and should we) compile blocks ahead-of-time (ie generate JS code for the block function) instead of generating an xml string representing the block and compiling it once per component instance. Note that this could be done in Owl 2 as it wouldn't change anything in terms of the API, profiling would be needed to understand how much time we spend compiling blocks (I expect it's not much compared to compiling the components)

sdegueldre avatar Mar 19 '25 00:03 sdegueldre

@ged-odoo ,

It would be incredible to see some of the sorts of features that are currently in the experimental branch in React such as the ones listed in this post: https://react.dev/blog/2025/04/23/react-labs-view-transitions-activity-and-more

See in particular:

  • View Transitions
  • Activity

Also being able to handle things like windowed/virtualised queries for very long list views would be fantastic, see: https://web.dev/articles/virtualize-long-lists-react-window https://github.com/bvaughn/react-window

luke-stdev001 avatar Apr 28 '25 15:04 luke-stdev001

being able to handle things like windowed/virtualised queries for very long list views would be fantastic, see: https://web.dev/articles/virtualize-long-lists-react-window https://github.com/bvaughn/react-window

This is generally implemented by library users, not the library itself. There are some virtualized lists and grid components implemented in Odoo for example: https://github.com/odoo/odoo/blob/14b53e0ef7738bfe0681ebf4f5eaedadf18bf9e8/addons/web/static/src/core/virtual_grid_hook.js

sdegueldre avatar May 02 '25 20:05 sdegueldre