Skip to content

Instantly share code, notes, and snippets.

@ged-odoo
Last active February 12, 2026 10:28
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 Note
1 useState removed Note
2 reactive removed 240 calls Note
3 useEffect semantics changed 596 calls Note
4 this.props removed Note
5 static props / defaultprops ignored (use the props function) 281 default props Note
6 this.env removed 161 useSubEnv Note
7 rendering context changes (reading from component through this) Note
8 onWillUpdateProps removed 183 calls Note
9 t-esc removed Note
10 t-ref changes: takes a signal (or resource) 1022 calls Note
11 t-model changes: takes a signal 197 calls Note
12 onWillRender removed 70 calls Note
13 onRendered removed 20 calls Note
14 this.render removed 130 calls Note
15 t-portal removed 18 calls Note
16 useExternalListener renamed to useListener (and changed) 210 calls Note
17 App has only sub roots 20 new App calls Note
18 loadFile removed Note
19 t-call not allowed on tags !== t Note
20 t-call body evaluated lazily, variables passed as parameters Note
21 useComponent removed 93 calls Note
22 t-slot renamed to t-call-slot 224 calls Note
23 validateType changed 1 call Note
24 validate removed 4 calls Note

Migration guide for each change

In this section, you will find a more detailed explanation on how owl 2 code can be converted for each individual breaking change listed above.

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
import { useState } from "@odoo/owl";

...

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

// owl 3
import { proxy } from "@odoo/owl";

...

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

There may still 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. Basically all uses of reactive with one argument ca be replaced by proxy.

// simple use
// owl 2
this.thing = reactive({...});
// owl 3
this.thing = proxy({ ... });

If a reactive function call uses a second argument, it is typically followed by some code that reads a value, so the reactive proxy is subscribed, and the second argument function will be called whenever the value has changed. This kind of code can be converted into a proxy call, and a useEffect or effect (depending if we are in a component/plugin, or in some kind of global situation).

In Owl 2.x, this looks like this:

this.state = reactive({...}, () => {
    // do something with this.state
    ...
    // subscribe again
    JSON.stringify(this.state);
});
JSON.stringify(this.state); // subscribe to all content

In Owl 3.x, this can be converted to code like this:

this.state = proxy({...});
useEffect(() => {
    // do something with this.state
    // be careful: unlike in owl 2, this function is always called immediately
    ...
    // no need to subscribe again
});
// no need to subscribe

But sometimes, the reactive second argument is used to compute a second piece of derived reactive state. In that case, there is an even better solution: we can simply uses a computed function.

// owl 2
this.derivedState = reactive({ double: 2 });
this.state = reactive({ count: 1}, () => {
    this.derivedState.double = 2*this.state.count;
    this.state.count; // subscribe
});
this.state.count; // subscribe

// owl 3
this.state = proxy({ count: 1 }); // could be a signal also
this.double = computed(() => 2*this.state.count);

useEffect semantic change

The previous useEffect function from Owl has been simplified: it does not take a second argument, all dependencies are automatically tracked using the standard reactivity system.

So most current uses of useEffect in owl 2 can simply be simplified by removing the dependency array:

// owl 2
useEffect(() => {
    // some code
}, () => [..., ...]);

// owl 3
useEffect(() => {
    // some code
});

However, in many cases, the useEffect function is used to recompute some kind of derived state. In that case, it is more efficient to simply use a computed value, if possible:

// owl 2
this.state = { double: 0 };
useEffect(() => {
    this.state.double = 2*this.props.somevalue;
});

// owl 3
// only works if we read values from signals and/or proxies
this.double = computed(() => 2* this.props.somevalue());

The previous implementation only depends on onMounted, onPatched and onWillUnmount, which still exists in owl 3. So, if there is some subtle reason for which the new useEffect does not work, one can fall back to the previous implementation by inlining it.

this.props removed

In Owl 2, each component has a built-in this.props, which contains everything given to it by the parent component. In Owl 3, this is no longer the case. Instead, a component has to explicitely "import" the props that it needs by calling the props function.

// owl 2
class MyComponent extends Component {
  static template = "...";

  setup() {
    // here, this.props is defined
  }
}

// in owl 3:
import { props, Component, ... } from "@odoo/owl";

class MyComponent extends Component {
  static template = "...";

  props = props();
  setup() {
    // here, this.props is defined, thanks to the props call
  }
}

Note that this does not perform any validation at all (see next point).

static props / defaultprops ignored

Most components in owl 2 define a static props object, that contains a description of the type of the expected props. Some component also have a static defaultProps.

Both of them can be added to the props object: the type description as the first argument, and the default values as the second argument. Here is what it looks like:

// owl 2.x
class SomeComponent extends Component {
  static template = "...";
  static props = {
      name: String,
      visible: { type: Boolean, optional: true },
      immediate: { type: Boolean, optional: true },
      leaveDuration: { type: Number, optional: true },
      onLeave: { type: Function, optional: true },
      slots: Object,
  };
  static defaultProps = {
    leaveDuration: 100
  }
}

// owl 3.x
class SomeComponent extends Component {
  static template = "...";

  props = props({
      name: t.string,
      "visible?": t.boolean,
      "immediate?": t.boolean,
      "leaveDuration?": t.number,
      "onLeave?": t.function(),
      // no need to grab the slot prop here
  }, {
    leaveDuration: 100
  });
}

Note that now, if you use a schema as the first argument, the props function will only return an object that contains the subset of keys that are defined. You can ignore props that you do not use, such as slots.

this.env removed

This one is a big change. The idea is that we can replace all uses of the env object with a set of plugins. A good conversion is not just mechanical, it requires to rethink the need and define one or more plugins.

As a first step, it is good to know that we can readd the env using the plugin system (see later in this document for some compatibility code), so this allows this migration to be done incrementally.

There are multiple ideas that are impacted by this change:

  • all current services will need to be replaced by corresponding (global) plugins
  • all useService call will need to be replaced by an import of the corresponding plugin
  • all useSubEnv should be replaced by providePlugins(...),
  • all useEnv should be replaced by plugin(SomePlugin)
  • all components that read something from the env should do something like this: this.thing = plugin(ThingPlugin)
// owl 2
// in some component A setup:
setup() {
  const someState = ...;
  useSubEnv({dashboardState: someState})
}

// in some child component:
setup() {
  const someState = this.env.dashboardState;
}


// in owl 3, we will probably write a plugin
class DashboardPlugin extends Plugin {
  state = ...
  // and maybe some other helpers, computed functions, whatever
}

// in component A
setup() {
  providePlugins([DashboardPlugin])
}

// in child component
setup() {
  const dashboard = plugin(DashboardPlugin);
  // here, we can read dashboard.state, or whatever
}

Rendering context changes

This change is also a large breaking change, but in theory, it can be mostly automated. We are going to provide migration scripts to do as much as possible of the work.

The main deal is to properly identify every variable that needs to be prefixed by "this.". The challenge for writing such a script is that some variables can come from a t-call, so they are not visible in the template that we are converting. But for most of Owl codebase, there are not so many t-call, so I expect that such a change will not be too difficult.

Manually, it is quite easy:

<!-- owl 2 -->
<t t-set="v" t-value="computeSomething()"/>
<div t-on-click="onClick"><t t-out="v"></div>

<!-- owl 3 -->
<t t-set="v" t-value="this.computeSomething()"/>
<div t-on-click="this.onClick"><t t-out="v"></div>

The good thing is that the owl 3 syntax is compatible with owl 2, so it is possible to do it before switching to owl 3.

onWillUpdateProps removed

In theory, onWillUpdateProps was used to provide a way for a component to react to a change in its props. In practice, it often mean that we actually want to define some computed state. This is the best case scenario. For example:

// owl 2
class C extends Component {
  static template = "...";
  
  setup() {
    this.state = useState({ 
      isLarge: this.props.counter > 10,
    });
    onWillUpdateProps((nextProps) => {
      this.state.isLarge = nextProps.counter > 10;
    });
  }
}

// owl 3
class C extends Component {
  static template = "...";
  
  props = props({ counter: t.signal(t.number) });
  isLarge = computed(() => this.props.counter() > 10);
}

Note that the prop counter had to be changed to a signal! This is actually quite important, so the computed value properly subscribe to the signal value.

Sometimes, we only want to react "once", for example, to reset a value. In that case, a useEffect is more appropriate

// owl 2
class C extends Component {
  static template = "...";
  
  setup() {
    this.state = useState({ someText: "" });
    onWillUpdateProps((nextProps) => {
      if (this.props.resId !== nextProps.resId) {
        this.state.someText = "';
      }
    });
  }
}

// owl 3
class C extends Component {
  static template = "...";
  
  props = props({ resId: t.signal(t.number) });
  someText = signal("");

  setup() {
    useEffect(() => {
      this.props.resId(); // subscribe to changes
      this.someText.set("");
    });
  }
}

Now, another common situation is when we are using the onWillUpdateProps hook to asynchronously load some value depending on the props. In that case, there is no really good way to solve the issue other than with a useEffect.

// owl 3
class C extends Component {
  static template = "...";
  
  props = props({ resId: t.signal(t.number) });

  setup() {
    useEffect(async () => {
      this.state = await this.loadRecord(this.props.resId()); 
    });
  }
}

Note that the code above is not concurrency safe, also, it is critical that we read the observed values immediately in the effect function. To solve these issue properly, we will provide a asyncComputed helper in Odoo:

// owl 3
class C extends Component {
  static template = "...";
  
  props = props({ resId: t.signal(t.number) });
  state = asyncComputed(() => this.loadRecord(this.props.resId()));
}

t-esc removed

This change is mostly a search and replace operation:

<!-- owl 2 -->
<div t-esc="this.value"/>

<!-- owl 3 -->
<div t-out="this.value"/>

It is in theory possible to have a markup string that we actually want to escape, but if that is really the case, then we can manually "unmarkup" it:

<div t-out="new String(this.value)"/>

Another more annoying issue is that in owl 2, the t-esc directive would call .toString if it receives an object. However, the owl 2 t-out directive will crash if given an object. So, if we want to prepare a migration ahead of time by changing all t-esc to t-out, then we need to handle these cases more carefully.

<!-- owl 2, with a value which may be an object -->
<t t-esc="someValue"/>
<!-- owl 2, with a t-out -->
<t t-out="window.String(this.someValue)"/>
<!-- owl 3, we can cleanup the template if we want -->
<t t-out="this.someValue"/>

Note that owl 3 will properly handle object values, so we only need to cast the object to a string for the duration when we run the code with owl 2.x.

t-ref changes: takes a signal (or resource)

The migration process here requires some thought to take advantage of the new system. However, a typical migration will be usually quite simple:

  • replace the useRef call by a new signal
  • update the t-ref directive in the template to point to the signal
  • update the code that was using the ref to the signal syntax (so, function call to read the value)
// owl 2
class C extends Component {
  static template = xml`<div t-ref="somename">...</div>`;

  setup() {
    this.ref = useRef("somename");
    onMounted(() => {
      console.log(this.ref.el);
    });
  }
}

// owl 3
class C extends Component {
  static template = xml`<div t-ref="this.div">...</div>`;

  setup() {
    this.div = signal(null);
    onMounted(() => {
      console.log(this.div());
    });
  }
}

t-model changes: takes a signal

This is similar to the t-ref change. A

// owl 2
class C extends Component {
  static template = xml`<input t-model="state.value"/>`;

  setup() {
    this.state = useState({ value: "coucou" });
  }
}

// owl 3
class C extends Component {
  static template = xml`<input t-model="this.input"/>`;

  input = signal("coucou");
}

But it requires changing the t-model expression to evaluate to a signal (a proxy will not work). So, all code that is using the value should be slightly adapted accordingly.

onWillRender removed

A common usecase for onWillRender is to precompute expensive value. In that case, the best migration is to convert the expression to a computed value.

// owl 2
class C extends Component {
  static template = xml`<t t-out="state.value"/>`;

  setup() {
    this.state = useState({ value: 0});
    onWillRender(() => {
      this.state.value = this.expensiveCoputation();
    });
  }
}

// owl 3
class C extends Component {
  static template = xml`<t t-out="this.value()"/>`;

  value = computed(() => this.expensiveComputation());
}

But to make it work, it should only depends on reactive values (signals/computed or proxies).

Sometimes, onWillRender is used to create some other side effects in the system. It is usually incorrect, since the fact that a component is rendered is not a good invariant. In that case, it is probably better to create the side effect in a mounted hook.

// owl 2
class C extends Component {
  setup() {
    onWillRender(() => {
      this.showNotification();
    });
  }
}

// owl 3
class C extends Component {
  setup() {
    onMounted(() => {
      this.showNotification();
    });
  }
}

onRendered removed

Usually, onRendered is used (incorrectly) to reset some state or do some control flow operation. Usually, the correct way to do it is to use onMounted or onPatched instead.

// owl 2
// Will render noContentView only at the first loading
onRendered(() => {
    this.loadHelper = false;
});

// owl 3
onMounted(() => {
    this.loadHelper = false;
});

Note that in this case, maybe using some smarter code, like a computed or an effect, is enough to make sure that we do not load twice the loadHelper, so maybe the onMounted call can even be removed.

this.render removed

In Owl, the normal way of updating the UI is through a correct use of the reactivity system, where each state change is intercepted by Owl and will result in an update of all corresponding components. However, as a safety measure, we added a render method on components, to make sure that each component can be forced to update, even without using the reactivity system.

Now, in owl 3, the new signal-based reactivity system feels much more powerful, and we hope that it is enough for all use cases. So, the normal migration process for this breaking change is to simply uses signals/reactive values.

// owl 2
class C extends Component {
  static template = xml`<t t-out="value"/>`;

  value = 1;

  someMethod() {
    this.value++;
    this.render();
  }
}

// owl 3
class C extends Component {
  static template = xml`<t t-out="this.value()"/>`;

  value = signal(1);

  someMethod() {
    this.value.set(this.value()+1);
  }
}

t-portal removed

useExternalListener renamed to useListener (and changed)

todo

App has only sub roots

todo

loadFile removed

The function has been simply removed. There are no use of that function in Odoo, but if you are using it, you can simply inline its definition, or define it in some util file in your project.

export async function loadFile(url){
  const result = await fetch(url);
  if (!result.ok) {
    throw new OwlError("Error while fetching xml templates");
  }
  return await result.text();
}

t-call not allowed on tags !== t

todo

t-call body evaluated lazily, variables passed as parameters

todo

useComponent removed

The useComponent was only useful in the context of a hook that wanted to get some value from the component or act on the component (usually a bad idea)

  • reading the env
  • reading the props
  • writing some value on the component

The first usecase is replaced by importing directly the corresponding plugin (if it makes sense).

The second use case can make a good use of the new props function:

// owl 2
const c = useComponent();
// do something with c.props

// owl 3
const props = props({ value: t.string});
// do something with props

And the last usecase should probably be done in a different way. For example, instead of writing a value on the component, we can return an object that contains the desired value. This is way more composable, and interact better with the reactivity system. For example, if we want to have the mouse coordinates on the component:

// owl 2
function useMouse() {
  const comp = useComponent();
  useExternalListener(window, "mousemove", ev => {
    comp.mouseX = ev.mouseX;
    comp.mouseY = ev.mouseY;
  });
}

// owl 3
function useMouse() {
  const mouseX = signal(0);
  const mouseY = signal(0);
  useListener(window, ev => {
    mouseX.set(ev.mouseX);
    mouseY.set(ev.mouseY);
  });
  return { mouseX, mouseY };
}

t-slot renamed to t-call-slot

A small usability issue with the t-slot directive was that it was not obvious if it is the place where we insert the content of the slot, or if we define the slot. In Owl 3.x, it has been renamed to t-call-slot, so the intent is more obvious.

  • t-set-slot defines the content of a slot
  • t-call-slot insert the content of a slot
<!-- owl 2 -->
<div class="header">
  <t t-slot="header"/>
</div>
<div>
  <t t-slot="body"/>
</div>


<!-- owl 3 -->
<div class="header">
  <t t-call-slot="header"/>
</div>
<div>
  <t t-call-slot="body"/>
</div>

validateType changed

validate removed

Validation functions changed in Owl 3.x, there are now two functions validateType and assertType.

  • validateType(value, type): check that the value matches the type and returns a list of issues if any
  • assertType(value, type): check that the value matches the type and throws an error if there is an issue
// owl 2
const errorMessage = validateType(value, String);
if (errorMessage) {
  throw new Error(errorMessage);
}

// owl 3
assertType(value, types.string);

Migration Plan for Odoo codebase

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

[Phase 1A, Phase 1B] => merge owl3 in master => Phase 2 
  • Phase 1: preparation
    • Phase 1A: prepare master by adding owl2_with_some_owl3 build, and replacing/rewriting unpatchable code
    • Phase 1B: create dev branch, add owl_3_with_some_of_owl2, compatibility layer
    • goal is to be able to merge quickly owl 3 in master, without disrupting too much the ongoing work in odoo
  • Phase 2: cleanup
    • progressively remove owl2 specific code and uses of compatibility layer
    • replace owl_3_with_some_of_owl2 by owl_3, celebrate

Branches:

The main strategy is:

  • work on master-owl3-migration branch (it already exists)
  • add owl3 in the dev branch
  • add a compabitility layer in addons/web/static/lib/owl/odoo_module.js
  • make owl3 as much compatible as possible to owl2 code
  • at the same time, prepare master to remove all non-patchable parts of owl2
    • add a web/static/src/owl2/utils.js file to put some owl2 code until cleanup is done
  • we want to have a small master-owl3-migration branch
  • as soon as we have a green set of branches => merge, go to phase 2, remove all uses of the compatibility layer progressively

Deadline: phase 1 starting feb 16 (after saas19.2 fork) until we merge in master just after 19.3 fork (so, somewhere around april 20th)

Phase 1: preparation

Here is a detailed list of tasks:

Change Master Master-owl3-migration
useState removed owl.useState = owl.proxy
reactive removed owl.reactive = owl.proxy or (val, fn) => { if(fn) throw Error; else return proxy(val) }. If error occurs, convert code to use useEffect from Odoo
useEffect copy Owl2 useEffect code to useLayoutEffect in @web/owl2/utils; remap all imports and uses to useLayoutEffect
this.props removed import props function, add props = props(); in each component with script. If possible, get static props and default props as well
this.env removed monkey patch env, useEnv, useSubEnv, useChildSubEnv using EnvPlugin
Rendering context changes use scripts to add this. to all free variables in components/templates
onWillUpdateProps removed remove some uses of onWillUpdateProps remove all uses of onWillUpdateProps
t-esc removed replace all t-esc with t-out using scripts
t-ref changed rename all t-ref β†’ t-custom-ref with scripts; add custom directive to remap t-custom-ref β†’ t-ref implement t-custom-ref in an Owl2-compatible way
t-model changed rename all t-model β†’ t-custom-model; add directive to remap t-custom-model β†’ t-model implement t-custom-model in a owl 2 compatible way
onWillRender removed remove some uses of onWillRender remove all uses of onWillRender manually
onRendered removed remove some uses of onWillRender remove all uses of onRendered manually
this.render removed export a render function in owl2/utils and update all uses to import this function
useExternalListener renamed implement owl.useExternalListener in owl2/utils; adapt all code to use it instead of useListener
t-portal removed remove all t-portal usage manually. Or/and keep support for t-portal in owl 3, temporarily
t-call restrictions prevent t-call on tags !== t using scripts
App sub roots adapt all instantiations of new App roots according to Owl3
t-slot renamed to t-call-slot rename all occurences
validateType changed adapt with owl3.validateType or owl3.assertType if throws an error
validate removed implement owl.validate in owl2/utils adapt with owl3.assertType

Phase 2: cleanup

  • useState: replace all imports/uses of useState by proxy
  • reactive: replace all imports/uses of reactive by proxy
  • useEffect: go through all uses of useLayoutEffect and replace them, if possible by useEffect from owl 3
  • this.props removed: add a linter to make sure we don't add static props/defaultprops back
  • this.env removed: go through all uses of env and rewrite them using plugins
  • rework all uses of t-custom-ref to remove them
  • this.render removed: remove all imports of the render function from owl2/utils
  • t-ref: go through all uses of t-custom-ref, and rewrite code to use a signal
  • t-model: go through all uses of t-custom-model, and rewrite code to use a signal
  • check all uses of useExternalListener in owl2/utils => replace them by useListener in owl3
  • remove all uses of t-portal (manual work)

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();
    }
}

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