Skip to content

Instantly share code, notes, and snippets.

@ged-odoo
Last active January 28, 2026 13:16
Show Gist options
  • Select an option

  • Save ged-odoo/0599d7cd9710428eaf97ea5b0e71f589 to your computer and use it in GitHub Desktop.

Select an option

Save ged-odoo/0599d7cd9710428eaf97ea5b0e71f589 to your computer and use it in GitHub Desktop.
owl3 migration guide

πŸ¦‰πŸ¦‰ Migration πŸ¦‰πŸ¦‰πŸ¦‰ Guide (DRAFT)

Overview

Since Owl 3 is a significant change from Owl 2, it will require a good amount of work to update Odoo (and other) codebases. This document is intended as a guide and set of resources to help everyone as much as possible.

This is a work in progress!!!

Table of Content

List of breaking changes

Additional info is the result of grepping in odoo code base (community/enterprise)

# Change Additional info
1 useState removed
2 reactive removed 240 calls
3 useEffect semantics changed 596 calls
4 this.props removed
5 static props / defaultprops ignored (use the props function) 281 default props
6 this.env removed 161 useSubEnv
7 rendering context changes (reading from component through this)
8 onWillUpdateProps removed 183 calls
9 t-esc removed
10 t-ref changes: takes a signal (or resource) 1022 calls
11 t-model changes: takes a signal 197 calls
12 onWillRender removed 70 calls
13 onRendered removed 20 calls
14 this.render removed 130 calls
15 t-portal removed 18 calls
16 useExternalListener renamed to useListener (and changed) 210 calls
17 status changed 64 calls
18 App has only sub roots 20 new App calls
19 loadFile removed
20 t-call not allowed on tags !== t
21 t-call body evaluated lazily, variables passed as parameters
22 useComponent removed 93 calls

Migration guide for each change

1. useState removed

This one is pretty easy: replace all uses of useState by proxy (in import statements and in code). This works, even though the underlying code of proxy uses signals.

For example

// owl 2
this.state = useState({ someValue: 1 });

// owl 3
this.state = proxy({ someValue: 1 });

There may be some change in behaviour, as some components will not need to be rendered in owl 3, since the reactivity system will be able to avoid subscribing to state updates in some cases (for example, in event listeners).

2. reactive removed

Almost like useState.

  • all uses of reactive with one argument can be replaced by proxy.
  • for uses of reactive with a second argument, a manual update has to be done, probably using effect or useEffect, or maybe using computed if it makes sense.
// simple use
// owl 2
this.thing = reactive({...});
// owl 3
this.thing = proxy({ ... });

useEffects semantic change

todo

and the rest...

Migration Plan for Odoo codebase

Roughly two main phases: a preparation phase, then we merge owl 3 in master, then a cleanup phase.

[Step 1A, Step 1B] => merge owl3 in master => Step 2 
  • Phase 1: preparation
    • Step 1A: prepare master by adding owl2_with_some_owl3 build, and replacing/rewriting unpatchable code
    • Step 1B: create dev branch, add owl_3_with_some_of_owl2, compatibility layer
  • merge: merge dev branch with owl_3_with_some_of_owl2 version in master
  • Phase 2: cleanup
    • progressively remove owl2 specific code and uses of compatibility layer
    • replace owl_3_with_some_of_owl2 by owl_3, celebrate

Phase 1A: preparations in master

Build owl2_with_some_owl3, add it in master:

  • export render function

  • useEffect:

    • duplicate current owl2 useEffect code in odoo useLayoutEffect (@web/owl2_utils?)
    • adapt all useEffect imports from @odoo/owl to @web/owl2_utils
  • rendering context: add this. on all free variables in all templates (NEED SCRIPT)

  • try to remove all uses of onWillUpdateProps

    • (need useAsyncEffect in odoo)
    • if successfull => remove onwillUpdateProps from owl3_with2 build
  • rename all t-esc => t-out (NEED SCRIPT)

  • t-ref:

    • rename them all t-custom-ref (NEED SCRIPT?), add directive to remap them to t-ref
  • t-model:

    • rename them all t-custom-model (with script?), add directive to remap them to t-model for now
  • this.render: update all uses to import render from owl and uses it

  • implement useExternalListener in owl2_utils

  • adapt code to use it instead of owl useListener (script?)

  • t-call: prevent t-call on tag !== t (need script)

Phase 1B: dev branch master-owl3

  • create dev branch master-owl3:
  • add owl3.js to web/static/libs/owl
  • create owl3_compatibility.js in web/static/libs/owl <-- need to be executed before owl code
  • owl_3_with_some_of_owl2 build is: owl 3 with additional temporary code:
    • t-portal support is not removed yet
    • this build should have a ComponentNode.prototype.beforeSetup method that is called before the setup, to make it patchable
    • keep onWillUpdateProps
    • change t-custom-ref and t-custom-model directive to wrap content of t-ref and t-model in a signal-like function

In compatibility layer:

// useState
owl.useState = proxy;

// reactive
owl.reactive = function(value, cb) { 
    if (cb) { 
        // depreciation warning => probably require manual code update
        console.warn("reactive is deprecated");
        useEffect(cb());  
    }
    return proxy(value);
}

class EnvPlugin extends Plugin {
  env = {};
}

const useEnv = () => plugin(EnvPlugin).env;
owl.useEnv = useEnv;

owl.useSubEnv = function (extension) {
  const env = useEnv();
  const subEnv = Object.assign(Object.create(env), extension);
  class SubEnvPlugin extends Plugin {
    static id = "EnvPlugin";
    env = subEnv;
  }
  providePlugins([SubEnvPlugin]);
}

owl.onWillRender = (cb) => {
    // find a way to make it work
}

owl.onRendered = (cb) => {
    // find a way to make it work
}

owl.useComponent = () => {
    ...
}
owl.useExternalListener = ... // duplicate current code from owl


owl.Component.ComponentNode.beforeSetup = function() {
    if (!this.component.props) {
        // only patch it if component does not define it before
        this.component.props = props();
    }
    if (!this.component.env) {
        this.component.env = useEnv();
    }
}
  • adapt all codes that instantiat new App and roots

Phase 2: cleanup

rewrite code to use owl 3

  • replace useState by proxy (NEED A SCRIPT)
  • replace reactive by proxy (NEED A SCRIPT)
    • all reactive uses of 2nd argument => manual code update to use useEffect
  • go trough all useEffect, and see if they can be replaced by owl useEffect
    • if yes => do it
    • if at the end, some useEffects cannot be converted => see if we need some notion of useLayoutEffect in owl3
  • props:
    • add props = props() in all components (NEED SCRIPT)
    • if possible, try to convert in script static props/defaultprops => props({ ...}, {...})
    • as soon as possible: remove this.props override in compatibility layer
    • check that we have no remaining static props/defaultprops, maybe add a linter to prevent readding them in forward ports
  • env
    • go through all uses of env
    • replace them by plugins/provideplugins/...
  • t-ref: go through all uses of t-custom-ref, and rewrite code to use a signal
  • remove all uses of onWillRender: manual work
  • remove all uses of onRendered: manual work
  • remove all uses of the exported render function: manual work
  • remove all uses of t-portal (manual work)
  • check all uses of useExternalListener in owl2_utils => replace them by useListener in owl3
  • remove all uses of useComponent

When we can:

  • Remove compatibility layer
  • replace owl3_with_some_of_owl2 by owl3

List of migration Scripts

Phase 1

  • replace all t-ref with t-custom-ref
  • add this. before all free variables in owl templates
  • rename t-esc => t-out (simple)
  • replace useState => proxy in all js code
  • replace reactive => proxy (except if second argument)
  • add props = props() or props = props(type, defaultprops) in all components

Phase 2

?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment