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!!!
- List of breaking changes
- Migration guide for each change
- Migration Plan for Odoo codebase
- Compatibility Layer
- List of migration Scripts
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 |
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.
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).
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 contentIn 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 subscribeBut 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);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.
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).
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 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
useServicecall will need to be replaced by an import of the corresponding plugin - all
useSubEnvshould be replaced byprovidePlugins(...), - all
useEnvshould be replaced byplugin(SomePlugin) - all components that read something from the
envshould 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
}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.
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()));
}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.
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
useRefcall by a new signal - update the
t-refdirective 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());
});
}
}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.
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();
});
}
}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.
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);
}
}todo
todo
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();
}todo
todo
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 propsAnd 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 };
}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-slotdefines the content of a slott-call-slotinsert 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>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 anyassertType(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);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_owl3build, 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 1A: prepare master by adding
- Phase 2: cleanup
- progressively remove owl2 specific code and uses of compatibility layer
- replace
owl_3_with_some_of_owl2byowl_3, celebrate
Branches:
- main dev branch on odoo community: https://github.com/odoo-dev/odoo/tree/master-owl3-migration
The main strategy is:
- work on
master-owl3-migrationbranch (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.jsfile to put some owl2 code until cleanup is done
- add a
- 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)
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 |
- 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)
// 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();
}
}
Phase 1
- replace all
t-refwitht-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()orprops = props(type, defaultprops)in all components
Phase 2
?