Owl 3 in Odoo 20: signals, plugins and what breaks, checked against the code

Signals, computed values and effects replace useState; plugins replace services and env; templates must say this. for component values. Géry Debongnie's two Odoo Experience 2026 talks on Owl 3, with each claim checked in the 20.0 code, which ships Owl 3.0.0-alpha.50 and a compatibility layer for Owl 2 code.

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.

Introducing Owl 3: The next version of Odoo JavaScript framework — Odoo · Watch on YouTube
Advanced reactivity in owl 3 — Odoo · Watch on YouTube

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 shallowEqual for 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 bare effect returns 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.Map and signal.Set give one signal per entry. proxy is the deep, recursive equivalent of Owl 2’s useState (Intro 20:00). untrack reads without subscribing, useOnChange runs a callback when one expression changes, and asyncComputed handles values that need an RPC (Reactivity 32:02, Reactivity 47:02).
  • Templates and the DOM: t-model is now a two-way binding between an input and a signal, and t-ref puts 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):

  1. 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.
  2. 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).
  3. Derive with computed, not with an effect that writes another signal. The effect version is briefly inconsistent (Reactivity 39:02).
  4. 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).
  5. Don’t wrap whole classes in proxy out 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).
  6. 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. usePlugin returns 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 props declaration. You call useProps() in setup with 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 (alias types), describes those shapes. validateType returns a list of problems, assertType throws (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.py handles what can be changed statically, such as t-esc to t-out, useState to proxy, t-ref, t-model and the this. prefixes (Intro 43:02). Run it with odoo-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.js puts 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:

Otherweb, website · 20.0c275e75d ↗

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, useState and most t-ref and t-model uses, but not everything (Intro 43:02).
  • useEffect changed 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 use useLayoutEffect while 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 props declarations become useProps() calls; default values move into the type description.
  • Patches on services: services are turning into plugins one by one in 20.0 updates, the ORM first. Check patches on core services after each update.
  • t-esc still works but logs a deprecation warning. Replace it with t-out; mind markup() 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

Found a mistake? ask@odoodigest.com · Corrections logAll changes in 20.0 →

My stack

Pick the versions and apps you run. Weekly digests, this week's table and All changes then show only what concerns you. Saved in this browser.

Versions
Apps
Modules