Odoo 20’s web client runs on Owl 3, a new major version of Odoo’s JavaScript framework that breaks Owl 2 code. At Odoo Experience 2026, Géry Debongnie, one of Owl’s main developers, gave two talks on it: an introduction to what changes and why, and a deep dive into the new reactivity engine. This guide is for developers who maintain custom JavaScript in Odoo modules. It sums up both talks, then checks every concrete claim against the Community code of 20.0. It is the front-end companion to our Python framework post; for the upgrade steps, see What’s new in Odoo 20.
The deep dive was scheduled an hour before the introduction (“you can watch the videos in reverse”, the speaker jokes). Read in order, start with the introduction. Below, Intro and Reactivity mark which talk a timestamp belongs to.
Owl 3 is still labelled alpha
The library in 20.0 is 3.0.0-alpha.50 (addons/web/static/lib/owl/owl.js), updated on October 23 (a3e934d6). 19.0 ships Owl 2.8.4. Odoo’s own code still partly runs through an Owl 2 compatibility layer. Expect the Owl API to keep moving in 20.0 updates.
Why a breaking version
Rendering is a solved problem, the speaker argues: Vue, Solid, React and Owl all render fast enough. What hurts in a code base as big as Odoo’s is state: derived values that every team caches by hand, with cache keys that may be wrong and that break when another module patches the code (Intro 1:00, Intro 10:00). Code like that is also hard for coding agents to work with, he adds. Owl 2 gave you one reactive tool, useState, tied to the lifecycle of components. Owl 3 moves the bookkeeping into the framework: you declare how values depend on each other, and Owl keeps them up to date (Intro 11:01, Reactivity 6:06).
What’s new
Signals, computed values and effects
- A signal is a value wrapped in a function. Call it to read the value, call
.set()to change it. Because every read goes through the function, Owl knows which computation read it and what to update when it changes (Intro 13:00). - A computed value derives from signals or other computed values. It is lazy: nothing runs until something reads it. It is memoised without a cache key. And if a recomputation gives the same result as before, nothing downstream is updated (Intro 19:03). In the speaker’s example, halving the quantity while doubling the price re-runs the total but triggers no effect (Reactivity 10:00). You can pass your own equality test, such as
shallowEqualfor a filtered list (Reactivity 26:02). - An effect runs a function now and again whenever what it read changes. Updates in the same call stack are batched, so the effect runs once, a microtask later. It can return a clean-up function (Intro 16:03, Reactivity 29:04). In components and plugins use
useEffect, which is disposed with its owner; a bareeffectreturns a stop function you must call yourself (Reactivity 30:02). - Scheduling: a component re-renders on the next microtask and reaches the DOM on the next animation frame; computed values run only when read; effects run eagerly (Reactivity 28:03).
- Helpers:
signal.Array,signal.Object,signal.Mapandsignal.Setgive one signal per entry.proxyis the deep, recursive equivalent of Owl 2’suseState(Intro 20:00).untrackreads without subscribing,useOnChangeruns a callback when one expression changes, andasyncComputedhandles values that need an RPC (Reactivity 32:02, Reactivity 47:02). - Templates and the DOM:
t-modelis now a two-way binding between an input and a signal, andt-refputs an element into a signal, so an effect can react when it appears (Intro 21:01).
Writing efficient code with signals
The deep dive ends with practical rules (Reactivity 35:01):
- Keep the signal wrapped as long as you can. Reading it in
setup()is not a subscription, so the template won’t update when it changes. Read it in the template. - Pass the signal, not its value, to child components. Then only the child re-renders when it changes, not the parent and every sibling (Reactivity 36:00).
- Derive with
computed, not with an effect that writes another signal. The effect version is briefly inconsistent (Reactivity 39:02). - Make the inputs reactive too. A computed value that mixes a plain value and signals goes stale when the plain value changes (Reactivity 41:01).
- Don’t wrap whole classes in
proxyout of Owl 2 habit. Each property read becomes a proxy lookup and creates signals that never change. Make only what changes a signal (Reactivity 43:02). - Wrap hot conditions in a small
computed, so the template re-renders only when the result flips, not when each input changes (Reactivity 45:01).
A computed value without a setter is a read-only view of your state: you can hand it to other code without letting it write (Reactivity 43:02). Avoid cycles in the graph; they end in “maximum call stack size exceeded” (Reactivity 34:00). A template still re-renders as a whole when one of its signals changes: Owl 3 does not update single text nodes (Intro 50:00).
Plugins replace services and env
Inspired by the plugin systems of the spreadsheet and the HTML editor, Owl 3 adds plugins: classes that extend Plugin, hold signals and use hooks such as onWillDestroy, like a component without a template (Intro 23:04, Intro 52:00). You get one with usePlugin(SomePlugin), passing the class rather than a string, so editors and type checkers know what you get and autocomplete its methods (Intro 26:02).
- A plugin can be global (registered for the whole app, like a service) or provided to one part of the tree with
providePlugins.usePluginreturns the nearest provided instance among the ancestors and fails if there is none (Intro 27:00). - Plugins can use other plugins, as long as there is no cycle (Intro 28:03).
- In Odoo 20 the ORM is already a plugin, and the other services will be converted “over the next few months” (Intro 26:02, Intro 30:03).
- Plugins can be patched like components, since they also use
setup(Intro 49:03).
Props, types and registries
- Props are no longer a static
propsdeclaration. You calluseProps()insetupwith the shape you expect, as often as you need, and get back only those props, with defaults. Other props are allowed (Intro 32:02). - Types: a small type language,
t(aliastypes), describes those shapes.validateTypereturns a list of problems,assertTypethrows (Intro 34:01). - Registries move into Owl as reactive registries, plus
Resource, an ordered set for items that need no key, such as navbar entries (Intro 35:00).
Templates read component values through this
In Owl 3 a template must say where a value comes from: this.counter for the component, a bare name for a template variable (Intro 14:02, Intro 30:03). The speaker calls it “kind of a crazy breaking change”, but it fixes bugs where Owl 2 confused the two. Odoo’s migration script follows t-calls across templates to add this. where it belongs (Intro 31:03).
Removed and renamed
From the speaker’s summary slide (Intro 36:00):
| Owl 2 | Owl 3 |
|---|---|
useState, reactive |
signals, computed, proxy |
env, useEnv, useSubEnv, useChildSubEnv, services |
plugins: Plugin, usePlugin, providePlugins, useScope |
static props |
useProps() and the t types |
t-esc |
t-out |
t-slot |
t-call-slot (and t-set-slot) |
useExternalListener |
useListener |
t-portal directive |
Portal component |
useEffect tied to rendering |
useEffect tied to the signals it reads |
t-ref, t-model on component state |
bindings to signals |
Migrating
The speaker points to three tools (Intro 41:00):
- Documentation: Owl 3 has a new documentation site with a release-notes page listing every breaking change, and a playground with tutorials. You can also hand these pages to a coding agent.
- Migration script:
odoo/upgrade_code/owl3-migration.pyhandles what can be changed statically, such ast-esctot-out,useStatetoproxy,t-ref,t-modeland thethis.prefixes (Intro 43:02). Run it withodoo-bin upgrade_code --addons-path=… --script owl3-migration, as described in What’s new in Odoo 20. - Compatibility layer:
web/static/src/owl2/owl3_compatibility_layer.jsputs Owl 2 APIs back on top of Owl 3, so you can migrate step by step (Intro 44:01).
Owl 3 first shipped in the Online-only version 19.4, with the layer. Odoo plans to remove the layer “in a few months”, around 20.1 or 20.2, the speaker guesses (Intro 45:03). Odoo has already started cleaning it up in 20.0:
Cleanup owl compatibility layer
Checked in the code
We read addons/web/static/lib/owl/owl.js in 20.0 (Owl 3.0.0-alpha.50) and in 19.0 (Owl 2.8.4), the compatibility layer, the migration scripts and Odoo’s own JavaScript. Of 23 claims: 19 confirmed, 2 different, 2 not checkable.
| Claim | Status | Where | Note |
|---|---|---|---|
| Odoo 20 runs Owl 3 | Confirmed | owl.js |
Version 3.0.0-alpha.50; 19.0 has 2.8.4. |
| Owl 3 arrived in 19.4, with a compatibility layer | Confirmed | efb95b76, ae8304ee | Both on May 22, 2026, when release.py said 19.4 alpha. |
Signals: call to read, .set() to write |
Confirmed | owl.js |
signal export. |
| Computed values: lazy, memoised, custom equality | Confirmed | owl.js |
computed(getter, { equals }); shallowEqual is exported. |
| A computed value is read-only unless given a setter | Confirmed | owl.js |
Its .set defaults to a read-only setter; a set option makes it writable. |
Effects: batched, with clean-up; useEffect bound to its owner |
Confirmed | owl.js |
effect, useEffect, plus immediateEffect and useImmediateEffect, which the talks didn’t cover. |
signal.Array, .Object, .Map, .Set; proxy replaces useState |
Confirmed | owl.js |
No add-on in 20.0 calls useState any more; the migration replaced it with proxy. |
untrack, useOnChange, asyncComputed |
Confirmed | owl.js |
asyncComputed is used in 11 files of Odoo’s add-ons. |
t-model and t-ref bind to signals |
Confirmed | owl3_compatibility_layer.js |
The layer renames the old behaviour to t-custom-model. |
Plugin, usePlugin, providePlugins |
Confirmed | owl.js |
Also useScope and Scope. |
| The ORM is a plugin | Confirmed | orm_plugin.js |
class ORM extends Plugin. |
| Services are being converted to plugins | Confirmed | hooks.js |
Owl’s Plugin is imported in 21 files of web (the speaker’s slide said 20) and 56 across the add-ons, while useService is still called in about 800 files. |
Templates need this. for component values |
Confirmed | 445da781 | Odoo ran the rendering-context script over its code in February 2026. |
useProps() with types and defaults |
Confirmed | owl.js |
Exports useProps, applyDefaults, getDefault. |
t/types; validateType lists problems, assertType throws |
Confirmed | owl.js |
|
| Odoo’s registry now comes from Owl and is reactive | Different | registry.js |
Owl 3 exports Registry and Resource, but Odoo’s registry is still its own class built on Owl’s EventBus; only its validation uses Owl’s assertType. |
Removed: useState, reactive, useEnv, useSubEnv, useChildSubEnv, useComponent, useRef, onWillRender, onRendered, status |
Confirmed | owl.js |
All exported by Owl 2.8.4, none by Owl 3. The compatibility layer adds back useEnv, useSubEnv and useComponent. |
t-esc removed |
Different | owl.js |
Still accepted: Owl 3 logs a deprecation warning and compiles it as t-out. |
t-call-slot, useListener, Portal component |
Confirmed | owl.js |
No t-portal or t-slot directive left. |
| Migration scripts total more than 3,000 lines | Confirmed | owl3-migration.py |
1,341 lines, plus 3,118 in the shared tools_js_expressions.py and 458 in tools_etree.py. |
Compatibility layer in web/static/src/owl2 |
Confirmed | owl3_compatibility_layer.js |
Its setup: t-portal → t-custom-portal, t-model → t-custom-model, useEffect → useLayoutEffect. |
| The layer goes in 20.1 or 20.2 | Not checkable | — | A plan. Odoo already trimmed it in 20.0 (c275e75d). |
| Owl 3 is slightly faster than Owl 2 in benchmarks | Not checkable | — | No benchmark in the repository. |
Community, Enterprise and Online
Owl and the compatibility layer are part of the Community web module, so everything above applies to Community, Enterprise and Odoo Online alike. Odoo’s Enterprise modules were migrated alongside: the Community commits above link matching odoo/enterprise pull requests. On Odoo Online, Owl 3 has been running since 19.4.
What developers should check
- Run the migration script first, then review its diff. It handles
this.in templates,t-esc,useStateand mostt-refandt-modeluses, but not everything (Intro 43:02). useEffectchanged meaning. In Owl 2 it ran after renders and took a dependency function; in Owl 3 it re-runs when the signals it reads change. Code that relies on the old timing should useuseLayoutEffectwhile it still runs through the compatibility layer, as the layer’s own setup notes say.- Template values: anything that comes from the component needs
this.. - Static
propsdeclarations becomeuseProps()calls; default values move into the type description. - Patches on services: services are turning into plugins one by one in
20.0updates, the ORM first. Check patches on core services after each update. t-escstill works but logs a deprecation warning. Replace it witht-out; mindmarkup()for HTML.- Pin your expectations: Owl is an alpha in
20.0, and the compatibility layer is temporary. Write new code in native Owl 3 (signals,computed, plugins) rather than through the layer.
Sources
- The two talks: Introducing Owl 3 and Advanced reactivity in Owl 3, Odoo Experience 2026, September 25 (event page).
odoo/odoobranches20.0and19.0:addons/web/static/lib/owl/owl.js,addons/web/static/src/owl2/,addons/web/static/src/core/orm_plugin.js,addons/web/static/src/core/registry.js,odoo/upgrade_code/owl3-migration.py.- Commits efb95b76 (Owl 3), ae8304ee (compatibility layer), 445da781 (rendering context), c275e75d (layer clean-up), a3e934d6 (Owl alpha.50).