Skip to content

Instantly share code, notes, and snippets.

@ged-odoo
Last active January 27, 2026 08:19
Show Gist options
  • Select an option

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

Select an option

Save ged-odoo/3d13eafc8ad19b79c4265adcc426ed98 to your computer and use it in GitHub Desktop.
DRAFT - 🦉owl 3🦉 Technical Notes - DRAFT

🦉OWL 3🦉Technical Notes (DRAFT)

Overview

Owl 3.x is the upcoming major version of Owl. This document provides a detailed overview of the proposed changes, along with explanations of the underlying ideas and motivations. Its primary purpose is to serve as a basis for discussion around the design decisions behind Owl 3.0.

Here is the big picture, in term of changes:

  • Rework the reactivity system to introduce signals, computed values, and effects.
  • Replace the env object with a plugin system.
  • Rework the props system to make it more powerful and better typed.
  • a redesign/simplification of other APIS (references, t-model, ...

Please note that the design is still a work in progress and may evolve as discussions progress.

This is a significant breaking change in Owl. Another gist contains a migration plan for odoo codebase: Migration Plan to Owl 3

Table of Content

Why Owl 3.x?

In the past, moving from the widget system to Owl 1.0 made it possible to keep scaling odoo without collapsing under our own complexity. Before Owl 1.0, the power/expressivity of the widget system was too low, so we had to manually coordinate all widgets changes in user code.

Owl 1 and 2 were about declaring components, and letting the framework coordinate them. With owl 2, you can let the framework compose components, and update the UI efficiently whenever the internal state changes. Without this transition, the development in the odoo javascript codebase would have slowed down considerably, as each change woul bring bugs, especially since it is very difficult to coordinate complex transitions across teams and applications. Also, it was difficult to attract and train talent.

Now, Owl 2 is a solid piece of code, and it is stable. So, why the need to change it? Well, in the last 3/4 years, the codebase at Odoo grew considerably, both in lines of code and in complexity. Complex systems have been built, and we encountered some "soft limits":

  • the reactivity simple, albeit simple in surface, is actually quite subtle, and difficult to diagnose bugs can appear, especially as more complex workflows are used in odoo
  • no good support for typing/autocompletion in props/env. So lots of work has been done to write or generate .d.ts files, docstring, and tooling.
  • difficulty to write performant code when we have to make computation based on reactive properties. Lots of tricks appeared to work around the issue (writing caches, using getters, using onWillRender to precompute values, ...). Sadly, they usually do not compose well (so, easy to break when code is patched from another addon for example).
  • not much guidance from the framework in the way we build application. It is nice not to be constrained, but at the same time, in Odoo, it makes it more difficult to understand other parts of the codebase, that may use different strategies/communication patterns.

So, to summarize, Owl 3.x is our answer to these challenges. We want to bring a little bit more power into the framework so future user code will be easier to write and maintain: computed functions to make state coordination more declarative, plugins to help coordinate parts of the application in a composable way, better APIS to improve typing/autocompletion situation, signals to solve all tricky reactivity issues.

Reactivity System

High-level change: the reactivity system based on proxies is replaced by a signal-based reactivity system, including signals, computed values, and effects.

  • useState and reactive are removed.
  • signal, proxy, and computed are introduced.
  • effect is introduced, and useEffect is simplified.

Unlike useState, signals (as well as computed values and proxy objects) are reactive values that are not tied to a specific component instance. Instead, they are automatically tracked and handled by the execution context in which they are accessed. For example, if a component reads a reactive value, it will be automatically re-rendered when that value changes. This will solve many subtle bugs and remove the need to call useState in many places.

Computed values are lazily evaluated and only recomputed when necessary—that is, when the value is accessed and one of its reactive dependencies has changed. This makes it straightforward to cache expensive computations efficiently.

const count = signal(0);
const state = proxy({ color: "red", value: 15});
const total = computed(() => count() + state.value);

console.log(total()); // logs 15

Owl components create an internal effect whenever they are instantiated and when they are rendered. So, this means that all reactive values read at another moment are not observed. For example, in Owl 2, an event handler could cause a component to subscribe to some values, which would possibly cause unintended updates later.

Signals

A signal is a value that can be observed (by reading it), and updated. It is created by the signal function:

const s1 = signal(3);           // a primitive value
const s2 = signal(new Model())  // whatever you want 

// read value by calling the function
s1(); // return 3
s2(); // return the model instance (NOT a proxy)

// set value with .set
s1.set(4);
// increment it
s1.set(s1() + 31);

Setting a signal to an identical value is ignored, so it does not trigger any component to be updated. However, it is something that we actually need sometimes. For example, if we push an element in a list. In that case, we need to explicitely invalidate the signal to tell Owl that everything that depends on it is now stale.

const list = signal([1,2,3]);

// does not change the content of the signal
list().push(4);
// so we have to manually tell owl it is no longer uptodate
signal.invalidate(list);

Since manipulating collections of elements is a very common need, we introduce four functions that basically wrap the target array, object, set or map in a proxy (but not a nested proxy like the proxy function). This is useful so we can properly invalidate the signal whenever the content has changed.

const mylist = signal.Array([]);
// changes the content of the list => owl will force updates 
mylist().push(4);

// mylist() is a proxy => the change is detected
mylist().push({nested: {object: 1}});

// here, we change deeply nested state => no change is detected, a manual
// invalidate may be necessary, if that is what we want
mylist().at(-1).nested.object = 2;

// same for other collections
const myobj = signal.Object({a: 3});
const myset = signal.Set(new Set(...));
const mymap = signal.Map(new Map());

Computed Values

A computed value is a value that is computed by a function, from other computed values and signals (and proxy). It will track its dependencies, and only be computed if necessary (when it is called, and is stale or some of its dependencies have changed);

const s1 = signal(3);
const s2 = signal(5);
const d1 = computed(() => 2*s1());
const d2 = computed(() => d1() + s2());

d1();  // evaluate the function, returns 6
d2();  // evaluate function d2, does not reevaluate d1, return 11
d2();  // returns immediately the result
s2.set(6);
d2();  // evaluate function d2, does not reevaluate d1, return 12

So, with computed values, we have a dynamic graph of values (base values like signals and proxies, and derived values with compute functions). Owl will try to efficiently only recompute what it needs.

Proxy

A proxy is the replacement for reactive/useState: it recursively returns a proxy, to make it easy to observe various nested structures. Note that it is not a hook: it can be called at any time.

const p = proxy({a: { b: 3}, c: 2}); //p is a proxy
p.a;  // another proxy that points to { b: 3 }
p.a.b; // 3

Effects

Signals, computed and proxy are values, they can be produced anywhere, at any time. Now, effects are functions that subscribe to values, they are the context in which a value is consumed. The effect function is executed immediately, and whenever the value it depends on change. It also returns a callback function to clean the effect (stop it).

About synchronicity: effects are executed immediately when created, and then, after a microtick, whenever they have to rerun.

const s = signal(3);
const d = computed(() => 2*s());
const cb = effect(() => {
  console.log(d()); // read the value d, which reads s
});
// at this point, 6 is logged
s.set(4);
// nothing happens immediately
await Promise.resolve();
// now 8 is logged => the effect has been reexecuted

cb();
// now the effect is inactive
s.set(5);
await Promise.resolve();
// nothing happens

Note that once created, an effect is always alive, so if you use it in a component, it should be cleaned up, typically in a onWillDestroy hook. To make it easy, that's exactly what useEffect does now:

class C extends Component {
  setup() {
    // equivalent to onWillDestroy(effect(() => {...}));
    useEffect(() => {
      console.log(someValue())
    });
  }
}

So, the useEffect hook is now tied to the signals instead of the component willPatch/patched hooks.

Why Signals?

Why change the reactivity system (useState/reactive)? The current system works well in most cases, solving roughly 95% of typical issues. However, in more advanced scenarios it can be tricky to use correctly. Developers need to carefully manage useState or toRaw calls and track how values propagate through the application.

Signals were chosen to replace proxy-based reactivity because they provide a simpler, more explicit, and more composable model.

With proxies, reactive behavior is implicit and can be difficult to reason about, especially when values cross abstraction boundaries. Signals make dependencies explicit at read time, which improves predictability and reduces surprising updates.

Because signals are not tied to components, they can be created and shared freely across the application, including in plugins and plain JavaScript modules. This leads to more flexible architectures and better separation of concerns.

Plugin System

The plugin system is designed to provide a higher level of abstraction than the environment. A plugin is a self-contained unit of code that can provide a service, maintain internal state, be imported by other plugins or components, and be dynamically installed on a component and its subcomponents. Plugins are automatically destroyed at the appropriate point in the component lifecycle.

Plugins have a simple lifecycle (setup => destroy), and as such, can call some hooks like components

The goal is to remain minimalist while offering a more type-safe alternative to the environment and a solid set of building blocks for large-scale applications. In Odoo, plugins will replace services.

The main ideas for plugins are:

  • the Plugin class: every plugin should subclass it,
  • the plugin function, useful to import a dependency in a typesafe way (either in a plugin, or in a component),
  • the PluginManager class: a class that instantiate and destroy plugins. It can have sub plugin managers, and each plugins in a child plugin manager has access to parent plugins.
class A extends Plugin {
  value = signal(1);
}

class B extends Plugin {
  // import the instance of A in the current plugin manager
  a = plugin(A);
  someFunction = computed(() => 2 * this.a.value());
}

const pm1 = new PluginManager({ plugins: [A,B]}); // A and B are instantiated

class C extends Plugin {
  b = plugin(B);
  f = computed(() => 2* this.b.someFunction());
}

const pm2 = new PluginManager({ parent: pm1, plugins: [C]}); // C is also instantiated
const c = pm2.getPlugin(C);
c.f(); // return 4

But in practice, one will not need to manipulate a pluginmanager. This is done automatically at the level of components:

class Clock extends Plugin {
  value = signal(1);

  setup () {
    const interval = setInterval(() => {
      this.value.set(this.value()+1);
    }, 1000);
    onWillDestroy(() => {
      clearInterval(interval);
    });
  }
}

class A extends Plugin {
  // import the instance of plugin Clock in A 
  clock = plugin(Clock); 
  mcm = computed(() => 2* this.clock.value());
}

class Root extends Component {
  static template = xml`<t t-out="this.a.mcm()"/>`;
  
  a = plugin(A); // import plugin A into component
}

mount(Root, document.body, { plugins: [Clock, A]})

Since the plugin system provides a more structured way to coordinate pieces of code, it is possible to replace all uses of the owl 2 environment with plugins. A service will become a global plugin, and useSubEnv can be replaced by sub plugins (with providePlugins).

import { Component, signal, mount, Plugin, plugin, providePlugins, computed, xml, onWillDestroy } from "@odoo/owl";

class Clock extends Plugin {
  value = signal(1);

  setup () {
    const interval = setInterval(() => {
      this.value.set(this.value()+1);
    }, 1000);
    onWillDestroy(() => {
      clearInterval(interval);
    });
  }
}

class A extends Plugin {
  // import the instance of plugin Clock in A 
  clock = plugin(Clock); 
  mcm = computed(() => 2* this.clock.value());
}

class ChildChild extends Component {
  static template = xml`<t t-out="this.a.mcm()"/>`;
  a = plugin(A); 

}

class Child extends Component {
  static components = { ChildChild };
  static template = xml`<ChildChild/>`;
  
  // cannot import A here => no provider
  // a = plugin(A); 
  setup() {
    providePlugins([A]);
    // can now import A
  }

}
class Root extends Component {
  static components = { Child };
  static template = xml`<Child />`;
  
  // cannot import A => no provider
  // a = plugin(A); 
}

// Clock is a global plugin (service)
mount(Root, document.body, { plugins: [Clock]});

Props System

Owl 3.x no longer uses the static props description or the static defaultProps object. Instead, props must be explicitly accessed through a function call. This approach is more direct, more composable (for example, a hook can grab some props), and better supported by IDEs, since types can be mostly inferred automatically.

import { Component, props, types as t } from "@odoo/owl";

class MyComponent extends Component {
  static template = "mytemplate";
  
  // now, this.props is an object with the two keys a and b, and IDEs can
  // infer that this.props.a is a string, and this.props.b is a optional number
  props = props({ a: t.string, "b?": t.number });
}

The props function can be called multiple times in a component:

import { Component, props } from "@odoo/owl";

class MyComponent extends Component {
  static template = "mytemplate";
  
  props = props({ a: t.string, "b?": t.number });
  otherProps = props({ c: t.instanceOf(SomeClass) });

  // no description here => we get all props received by the component
  // no type inference nor validation here!
  allProps = props();

  // short version, no type inference, but some validation
  propsabc = props(t.object(["a", "b?", "c"]));

  // we can define default values as well, as second argument:
  myProp = props({
    "foo?": t.boolean,
  }, { 
    foo: true
  });
}

Props validation is performed (in development mode) at each props function call, but only for the props explicitly defined in the component's props description. This represents a small philosophical change: we no longer validate or care about extra props passed to a component. An Owl component simply declares, via its props function calls, which props it expects to receive. It is more ergonomic for some cases, for example, we no longer need to declare the slots prop, unless we want to explicitely use it.

Although this change is minor in terms of framework code, it has significant implications for usability and developer experience. It becomes easier to subclass a component and add extra props, and the system is more type-safe and IDE-friendly.

Small note: the app config key warnifnostaticprops has been removed.

Notes on code migration (owl 2.x -> 3.x)

The simple upgrade process is this:

  • import the props function
  • replace the static props description object by a call to the props function, adding default values if necessary.
  • maybe remove unneeded props (like slots)
// 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
  });
}

Later, when the code is reworked, it may make sense to split the props in various props calls, for example if we have a set of props that we want to grab to give to a sub component.

Other Changes

env is removed

Plugins are a minimal abstraction that allow, in a typesafe way, to share state and features among various part of the code. They also support shadowing ( so, replacing all plugins imported in a component and its children by a different implementation), and also, can be added at runtime, in any component. As such, they provide a more powerful abstraction than the env object.

This is a big change in the way we write and coordinate code in Odoo. Obviously, this cannot be done instantly, so we plan to temporarily keep support for the env object.

We are aware that this is a very big change, and will require a complicated code upgrade, but from our early work, it feels that the resulting code is much simpler.

Compatibility code for keeping `env`

Here is a rough outline of what the compatibility code could look like:

class EnvPlugin extends Plugin {
  env = {};
}

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

patch(Component.prototype, {
  setup() {
    super.setup();
    this.env = 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]);
}

Services are removed

Okay, this is quite a provocative title. First, services are not a Owl concept, so there is not really a change in Owl 3. However, with the arrival of plugins, all services will likely be converted to plugins. It may look like this:

// router service
class RouterPlugin extends Plugin {
  ...
}

// action service
class ActionPlugin extends Plugin {
  // import router service
  router = plugin(RouterPlugin);
  ...

  doAction(action) {
    ...
    this.router.navigateTo(newUrl);
  }
}
// notification service
class NotificationPlugin extends Plugin {
  ...
  add(title, text) {
    ...
  }
}

// in a component:
class MyComponent extends Component {
  // import services
  notification = plugin(NotificationPlugin);
  action = plugin(ActionPlugin);

  doSomething() {
    // call services
    this.notification.add("Coucou", "some message");
    this.action.doAction(...);
  }
}

Rendering context

Change: in owl 3.x, all variables are by default local. Accessing the component can only be done with this.. For example:

<!-- owl 2.x -->
<t t-set="item" t-value="123">
<button t-on-click="onClick">
  <t t-out="val"/>
  <t t-out="item"/>
</button>

should be rewritten like this:

<!-- owl 3.x -->
<t t-set="item" t-value="123">
<button t-on-click="this.onClick">
  <t t-out="this.val"/>
  <t t-out="item"/>
</button>
Details

In Owl 2.x, the rendering context was an object whose prototype is the instance of the component.

<!-- owl 2.x -->
<div><t t-out="val"/></div>

So, the t-out expression above will read val from the template, if it is defined, and will fall back to the component if not.

In Owl 3.x, the rendering context is an object with a key this pointing to the instance of the component. So, in the following example, the first t-out will always read the value from the component, and the second from the local variable

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

This means that pretty much all js expressions in templates will have to be changed to use the proper this. expression.

<!-- owl 3.x -->
<div t-on-click="this.onClick">...</div>

The main motivation is to prevent capturing unexpectedly the rendering context. We have to sometimes use explicitely this to avoid issues. For example:

<button t-on-click="item => onClick(item)">button</button>

will call the onClick method from the rendering context! So, it will sort of work, the actual component method will be called, but bound to the context, not the component. It can lead to surprising bugs. For example, if the onClick method does something like this: this.a = 1, the a value will not be assigned to the component. This is a case where it is actually quite rare to get, but then you need to be an expert to understand the problem.

Another issue that this change will solve is to make it simpler to understand where a value comes from.

See odoo/owl#1317

Notes on code migration (owl 2.x -> 3.x)

This is clearly a significant breaking change. Most templates will need to be updated to prepend this. to each value that is not a local template variable. We will provide scripts to automate much of this work. However, it is not always possible to statically determine whether a variable comes from the component or from a local template variable.

The main complication arises when a variable in template A is defined in another template that calls A via t-call. Static analysis cannot always detect such cases. As a result, the migration script will typically assume that any unbound variable in A belongs to the component rather than being provided by the calling template.

In practice, this affects only a small portion of the Owl codebase, since most code tends to use subcomponents rather than relying heavily on t-call.

Type Validation

The type validation system has been improved. Owl now exports two functions:

// check that value satisfies the given type. returns a list of errors
// (so, empty list => ok)
validateType(value: any, type: any): Issue[];

// throw error if the value does not satisfy the given type
assertType(value: any, type: any, errorHeader?: string);

Instead of now defining a type with a mini dsl using objects, we now uses functions, that makes it easier to compose and manipulate types, and allow IDEs to know the correct type, so autocompletion will work much better. Here is what it looks like:

// owl 2.x
static props = {
  mode: {
      type: String,
      optional: true,
  },
  readonly: { type: Boolean, optional: true },
  onChange: { type: Function, optional: true },
  onBlur: { type: Function, optional: true },
};

// owl 3.x
props = props({
  "mode?": t.string,
  "readonly?": t.boolean,
  "onChange?": t.function(),
  "onBlur?": t.function(),
});

// other examples
props = props({
  someObject: t.object({
    id: t.string,
    values: t.union([t.array(), t.number])
  }),
});

assertType(myObj, t.object({ id: t.number, text: t.string}));

onWillUpdateProps is removed

The goal of onWillUpdateProps was to allow a component to react properly to props change. However, i believe that it is not the best solution for a framework. One of the main issue is that it is somewhat imperative (we have to explicitely tell the component what to do in order to maintain a coherent state), it would be much better if we could have a declarative way to do it, and we actually have such a tool now in owl 3: the reactivity system, and in particular, computed functions.

Now, it is possible to declare the relation between a component internal state and its props using computed functions. Also, a component can take a signal value as a prop, or the value of the signal. Note that it is usually better to give the signal from a performance standpoint.

class Child extends Component {
  static template = xml`<t t-out="this.double()"/>`
  props = props({ count: t.signal(t.number)});

  // double is always updated
  double = computed(() => 2*this.props.count());
}

class Parent extends Component {
  static template = xml`<Child count="this.count"/>`;

  count = signal(1);

  setup() {
    useInterval(() => this.count.set(this.count()+1), 1000);
  }
}

In the example above, only the child component will be updated when the count signal is incremented.

useComponent is removed

The useComponent hook is often problematic. This is sometimes useful to access the current app (by using comp.__owl__.app!), but this usecase has been replaced by useApp. From a quick look into Odoo codebase, we noticed that most uses of useComponent are not very good (for example, writing functions on the current component, or getting the current props), and can be easily replaced by better code (the props function can get the props).

t-esc is removed

The t-esc directive, previously used to output and escape text in templates, has been removed in Owl 3.x. It should now be replaced with the more powerful t-out directive.

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

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

Migration is straightforward: a simple search-and-replace of t-esc with t-out is usually sufficient.

t-slot is renamed 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
<div class="header">
  <t t-call-slot="header"/>
</div>
<div>
  <t t-call-slot="body"/>
</div>

Registries and resources

The Registry class from Odoo has been moved to Owl and rewritten to represent an ordered collection of key/value pairs. It is implemented using signals and computed functions, allowing it to integrate seamlessly with the reactivity system. While it could have remained in Odoo, having a minimal, fully typed, self-contained implementation makes it easy to use in any Owl-based project (such as o-spreadsheet or others).

The main advantages of registries compared to signal.Object or signal.Map is that it supports type validation, and that it is ordered (with sequence option).

const registry = new Registry({ name: "my registry"});
const obj = { a: 1};
registry.add("key", obj);
registry.get("key") === obj;            // true
registry.get("otherkey");               // throw error
registry.get("otherkey", 1) === 1;   // true

Registries are ordered, and support a sequence option number (default is 50):

registry.add("key1", 1, { sequence: 80 });
registry.add("key2", 2, { sequence: 20 });
registry.add("key3", 3, { sequence: 40 });
console.log(registry.items()); // => 2, 3, 1
More details on Registry

Here is the complete Registry class API:

class Registry<T>
  add(key: string, value: T, options);
  addById(item: T & {id: string}, options);

  get(key, defaultValue?: T);
  delete(key);
  has(key): boolean

  // returns the ordered list of [key,value] pairs
  entries: () => [string,T][];

  // returns the list of items
  items: () => T[];

We can validate entries:

// only accept strings
const registry = new Registry({ validation: String });
registry.add("string"); // works
registry.add(1);        // throws error

// only accept plugins
const pluginRegistry = new Registry({ 
  name: "plugin registry",
  validation: { extends: Plugin }
});
registry.add("str");   // throws error
registry.add(MyPlugin) // works

A common usecase is to add object with an id key. In that case, instead of doing this:

registy.add(item.id, item);  // it still works!

we can do this:

registry.addById(item);

Items and Entries are computed functions, so they work well with the reactivity system:

effect(() => {
  // will log everytime there is a change
  console.log(registry.items());
});

Another change from Odoo registries: there is no builtin concept of "subregistries", so, in other words, the "category" method does not have an equivalent. If we want a registry of registries, we have to define each subregistry explicitely.

We also introduce a new Resource class, which represents an ordered collection of items (a set, not a map). It is useful in many situations. For example, a Resource could represent a list of error handlers, systray items, command palette commands, keyboard shortcuts, and similar collections.

const resource = new Resource({ name: "error handlers" });

resource.items();       // []
resource.add("value");
resource.items();       // ["value"]
resource.has("value");  // true
resource.has("string"); // false

The main difference between a Resource and a set or list, is that the Resource class represents a live collection of items: resource.items is a reactive value (computed function), so one can write other computed functions or effects that depends on the content of a resource.

The main advantages of resources compared to signal.Array or signal.Set is that it supports type validation, and that it is ordered (with sequence option).

More details on Resource

Here is the complete Resource class API:

class Resource<T>
  add(item: T, options);
  delete(item: T);
  has(item: T): boolean

  // returns the list of items
  items: () => T[];

References

The reference system is simplified, using signals.

In owl 2.x, it works like this:

  • we tag a template element with t-ref,
  • we define a ref object with useRef in setup, giving it the name of the ref
  • we can access the value in ref.el, when the component is mounted
class C extends Component {
  static template = xml`<div t-ref="somename">...</div>`;

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

In owl 3.x, it works like this:

  • we define a signal
  • we give the signal to the t-ref element in the template
  • we can access it like a normal signal
class C extends Component {
  static template = xml`<div t-ref="this.ref">...</div>`;

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

This change provides a more powerful and flexible API. For example, it is now easier for a child component to pass a reference back to a parent: the parent can provide a signal to the child, which can then use it directly in t-ref. It also simplifies usage within computed functions and effects.

The reference system can also be used for multiple elements. In that case, one can use a Resource instead of a signal:

class C extends Component {
  static template = xml`
    <t t-foreach="this.list" t-as="item" t-key="item.id">
      <div t-ref="this.refs">...</div>`;
    </t>

  setup() {
    this.list = ...;
    this.refs = new Resource();
    onMounted(() => {
      console.log(this.refs.items());
    });
  }
}

t-model

In Owl 2.x, t-model was somewhat awkward: it required both a value to display and a way to update that value. This was handled using an "assignable expression":

<!-- owl 2.x this works-->
<input t-model="state.value"/>

<!-- owl 2.x and this does not work! (expression is not assignable)-->
<t t-set="v" t-value="state.value"/>
<input t-model="v"/>

In Owl 3.x, we now have signals, which represent updatable values. This allows us to pass a signal directly to a t-model expression. The approach is more powerful: signals are composable (a component can provide a signal to a child component, which can use it in its template), and they integrate seamlessly with computed functions and effects.

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

  value = signal("coucou");

  someValue = computed(() => this.value() + "!!!");

  setup() {
    useEffect(() => {
      // will be executed everytime the computed value changes, which is whenever
      // the value signal changes!
      console.log(this.someValue());
    });
  }
}

onWillRender and onRendered are removed

The onWillRender and onRendered hooks have been removed for several reasons:

  • They were often used to precompute expensive values, which is now better handled using computed functions.
  • They encourage an imperative approach: we want code to depend on the graph of reactive values, not on the rendering lifecycle of a component.
  • There are few use cases in the codebase, and the existing ones can typically be implemented using other hooks, such as onPatched or setup.

Overall, these hooks provided little value and could even encourage bad patterns.

this.render is removed

The manual this.render function is used to force owl to render a component. It is kind of a manual escape hatch, and was necessary for use cases where we want to bypass the reactivity system. Now in Owl 3, the reactivity system based on signal feels like it is sufficient for these usecases. Here is how we could do.

// owl 2
class MyComponent extends Component {
  static template = xml`<Renderer model="this.model"/>`
  setup() {
    this.model = new VeryBigModel({onUpdate: () => this.render()});
  }
}

// owl 3
class MyComponent extends Component {
  static template = xml`<Renderer model="this.model"/>`
  setup() {
    this.model = signal(new VeryBigModel({onUpdate: () => {
      signal.invalidate(this.model);
    }}));
  }
}

t-portal is removed

The t-portal directive is technically quite complex, and it feels that it does not bring enough value compared to the problems it can cause. In a world with declarative reactivity, we have enough other tools that are more robust. So, it was suggested to remove t-portal.

This could mean that instead of portalling some content to the system, we have to organize the code in a way that we can receive the portalled value as a component that we insert dynamically, maybe using roots.

// owl 2.x
class Something extends Component {
  static template = xml`
    <div>...</div>
    <div t-portal=".someselector">portal content</div>
  `;
}

In owl 3.x, we can do something like this instead:

class PortalPlugin extends Plugin {
  add(selector, component, props) {
    // mount here a new root at selector location
  }
}

function usePortal(selector, component, props) {
  const portal = plugin(PortalPlugin);
  const remove = portal.add(selector, component, props);
  onwillDestroy(remove);
}

class PortalContent extends Component {
  static template = xml`
    <div>portal content</div>
  `;
}

class Something extends Component {
  static template = xml`
    <div>...</div>
  `;

  setup() {
    usePortal(".someselector", PortalContent);
  }
}

useExternalListener is renamed useListener

The useExternalListener hook has been renamed to useListener. The word external was intended to indicate listening to events outside the component's DOM. However, the hook actually works with any EventBus, and in some cases it was awkward to call useExternalListener on an EventBus that a component actually owns. Renaming the function clarifies that it is a generic, reusable listener hook.

Also, the semantics of the hook has been changed: instead of adding an event listener in mounted and removing it when unmounted, it now adds immediately the event listener, and remove it when destroyed. The reason for that change is that it is usually an error to wait for mounted before starting listening to change. Also, it makes the hook useful for plugins, since plugins have a destroy lifecycle event, but not mounted/unmounted.

status

Has been changed, to work with plugins as well. It is now a hook.

=> not sure about this one, need to add more detail

App and Roots

In Owl 3.x, the App class has no longer a main root and sub roots, it only has sub roots.

// owl 2.x
class SomeComponent extends Component { ... }

const app = new App(SomeComponent);
await app.mount(target, { props: someProps });

should be rewritten like this:

// owl 3.x
class SomeComponent extends Component { ... }

const app = new App();
const root = app.createRoot(SomeComponent, { props: ...})
await root.mount(target);

Note that the mount helper function is not impacted.

Details

In Owl 2.X, an owl App has a main root component. We had to introduce the concept of sub roots to allow different roots to share the same settings (dev mode, translate function, compiled template cache, ...):

// owl 2.x
class SomeComponent extends Component { ... }
class SubComponent extends Component { ... }

const app = new App(SomeComponent);
await app.mount(target, { props: someProps });
const subRoot = app.createRoot(SubComponent);
await subRoot.mount(otherTarget, { props: someOtherProps});

In Owl 3.x, we no longer have a "main" root and "sub" roots. The code is simplified by just maintaining a set of roots. So, the API for the App class has slightly changed accordingly.

This change is motivated by making it slightly more ergonomic to use Owl with multiple roots. Note that in Odoo, pretty much all use cases of mounting sub roots or apps should probably be done by using roots. It is less efficient to have multiple apps, and could lead to difficult bugs if components/props from one app are used in the other.

useApp

A new hook is introduced: useApp, to get the current active owl App. It was not done in Owl 2.x because there was not really a big need to get the current App initially. However, since we introduced roots, it became useful to have a proper way to get the app.

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

  setup() {
    const app = useApp();
    const root = app.createRoot(SomeOtherComponent);
    root.mount(targetEl);
    onWillDestroy(() => {
      root.destroy();
    });
    
  }
}

loadFile is removed

The loadFile function was used to load a static file using fetch. It was actually useful to implement the playground application, and was added to Owl, but in practice, it is not very useful, and does not feel that it belongs to a UI framework.

Other ideas

We discuss here some other ideas that we explored and decided not to include in Owl 3.

Make it easy to output a simple reactive value

Right now, outputting a signal (or computed value) needs to go through a function call:

<div><t t-out="this.someValue()"/></div>

This is necessary due to the way signal works. However, we could add some code that would "magically" check for any value in an expression, and if it is a signal or a derived function, call it, so the following xml could work

<div><t t-out="this.someValue + this.otherValue"/></div>

This could be nice on a superficial level, but issues quickly arise, what if we want to give a signal (not the value) to a subcomponent?

<SomeComponent value="this.value"/>

If we automatically call the signal function, then it does not do the intended effect: the parent component is subscribed to the signal, and the child component get the value, not the signal. So we would need a opt out mechanism. And this is very common, so at the end, it is not clear that we have an improvement.

The current consensus seems to be that doing so is a little bit too magic, will only save a few visible parenthesis, and may make it harder to understand what is going on in a template. Also, it may make it harder for static tooling to work (in this case, the type of all signals is basically erased from the content of the template).

Simplified way to define a prop

It is often useful to only import a single prop:

class TodoItem extends Component {
  todo = props({ todo: t.instanceOf(Todo)}).todo;
}

class MyPlugin extends Plugin {
  editable = plugin.props({editable: t.signal()}).editable;
}

Maybe it would be worth it to have a special simplified syntax for this usecase:

class TodoItem extends Component {
  todo = prop("todo", t.instanceOf(Todo));
}

class MyPlugin extends Plugin {
  editable = plugin.prop("editable", t.signal());
}

It's nicer and remove the repetition, but at the cost of adding yet another primitive function in Owl.

Migration to Owl 3.x

Upgrading Owl 2.x code to use signals should mostly be simple:

  • useState can be replaced by proxy
  • reactive function calls can be replaced by proxy as well
  • getters, and manual code that maintains cache and other derived computations can be adapted in computed functions.
  • some care should be done with code that manages effects. Usually, if the effect in owl 2.x is there to compute something that depends on some reactive values, then it should be migrated into a computed function, not an effect (this is more performant, and better explains the intent)

Examples

This section showcases examples illustrating Owl's new features.

Example: a counter

Be careful when outputting a

class Counter extends Component {
    static template = xml`
      <div class="counter" t-on-click="this.increment">
        Count: <t t-out="this.count()"/>
      </div>`;
    
    count = signal(0);
    
    increment() {
        this.count.set(this.count() + 1);
    }
}
Example: a dynamic list of counters (showcase: signals, proxy, computed)

This example shows how to use a computed value in components:

import { Component, signal, mount, computed, xml, types as t, proxy, props } from "@odoo/owl";

class Counter extends Component {
    static template =  xml`
      <div class="counter" t-on-click="this.increment">
        Count: <t t-out="this.props.count()"/>
      </div>`;

    props = props({ count: t.signal(t.number) });

    increment() {
        this.props.count.set(this.props.count() + 1);
    }
}

class Root extends Component {
    static components = { Counter };
    static template = xml`
      <div>
        <p>Current sum: <t t-out="this.sum()"/></p>
        <button t-on-click="this.addCounter">Add a counter</button>
      </div>
      <t t-foreach="this.counters" t-as="counter" t-key="counter_index">
        <Counter count="counter"/>
      </t>
    `;

    counters = proxy([signal(1), signal(2)]);
    sum = computed(() => this.counters.reduce((acc, value) => acc + value(), 0));

    addCounter() {
        this.counters.push(signal(0));
    }
}

mount(Root, document.body);
Complex app (html editor), with various plugins
// In this example, we show how components can be defined and created.
import { Component, signal, mount, useApp, xml, Resource, plugin, Plugin, props, onWillDestroy, useEffect, useListener, providePlugins, computed, types as t } from "@odoo/owl";

// -----------------------------------------------------------------------------
// Notification system
// -----------------------------------------------------------------------------

// Exemple of a global plugin
class NotificationPlugin extends Plugin {
    static nextId = 1;
    notifications = signal.Array([]);
    
    setup() {
        const app = useApp();
        this.root = app.createRoot(NotificationManager).mount(document.body);
        onWillDestroy(() => this.root.destroy());
    }

    add(title, message) {
        const id = NotificationPlugin.nextId++;
        this.notifications().push({ id, title, message });

        setTimeout(() => {
            const notifs = this.notifications().filter(n => n.id !== id);
            this.notifications.set(notifs);
        }, 3000);
    }
    
    hasNotifications = computed(() => this.notifications().length);
}

class Notification extends Component {
    
    static template = xml`
        <div style="width:200px;background-color:beige;border:1px solid black;margin:5px;">
            <h3><t t-out="this.props.notification.title"/></h3>
            <div><t t-out="this.props.notification.message"/></div>
        </div>`;
    props = props({ 
        notification: t.object({title: t.string, message: t.message}) 
    });
}

class NotificationManager extends Component {
    static components = { Notification };
    static template = xml`<div t-if="this.notification.hasNotifications()" style="position:absolute;top:0;right:0;">
        <t t-foreach="this.notification.notifications()" t-as="notif" t-key="notif.id">
            <Notification notification="notif"/>
        </t>
    </div>`;
    
    notification = plugin(NotificationPlugin);
}
// -----------------------------------------------------------------------------
// Editor
// -----------------------------------------------------------------------------

class ContentPlugin extends Plugin {
    el = plugin.props( { editable: t.signal()}).el;
}

class SelectionPlugin extends Plugin {
    selectionHandlers = new Resource();
    content = plugin(ContentPlugin);
    selection = signal(this.getSelection());
    
    setup() {
        useListener(document, "selectionchange", () => {
            const el = this.content.el();
            const selection = document.getSelection();
            if (!selection || !el) {
                return;
            }
            const { anchorNode, focusNode } = selection;
            if (
                anchorNode && focusNode &&
                el.contains(anchorNode) &&
                (focusNode === anchorNode || el.contains(focusNode))
            ) {
                this.selection.set(this.getSelection());
            }
        });
    }

    getSelection() {
        const s = document.getSelection();
        return { 
            anchorNode: s?.anchorNode,
            anchorOffset: s?.anchorOffset || 0,
            focusNode: s?.focusNode,
            focusOffset: s?.focusOffset || 0,
            isCollapsed: s?.isCollapsed,
            text: s?.toString() || "",
        }
    }
}

class TextToolsPlugin extends Plugin {
    toggleBold() {
        // yeah, i know, but it's just a demo
        document.execCommand("bold");
    }
}

class GEDPlugin extends Plugin {
    selection = plugin(SelectionPlugin).selection;
    notification = plugin(NotificationPlugin);

    setup() {
        useEffect(() => {
            const s = this.selection();
            if (s.text === "ged") {
                this.notification.add("GED", "GED")
            }
        });
    }
}

class Editor extends Component {
  static template = xml`
    <div style="border:1px solid gray;">
      <div style="border-bottom:1px solid gray;">
        <span>Toolbar</span>
        <button t-on-click="() => this.textTools.toggleBold()">Bold</button>
      </div>
      <div contenteditable="true" style="height:500px;" t-ref="this.editable">
        <h3>Editable zone</h3>
        <p> this can be edited. Try to select the word "ged"</p>
      </div>
    </div>`;

  editable = signal(null);
  setup() {
    providePlugins([ContentPlugin, SelectionPlugin, TextToolsPlugin, GEDPlugin], {
      ContentPlugin: { el: this.editable }
    });
    this.textTools = plugin(TextToolsPlugin);
  }
}


// -----------------------------------------------------------------------------
// Editor
// -----------------------------------------------------------------------------


class MyApp extends Component {
  static components = { Editor, Notification };
  static template = xml`
    <div style="margin-bottom:10px;">
        <button t-on-click="() => this.toggleEditor()">Toggle Editor</button>
        <button t-on-click="() => this.addNotification()">Add Notification</button>
    </div>
    <Editor t-if="this.isEditorVisible()"/> `;
  
  notifications = plugin(NotificationPlugin);
  isEditorVisible = signal(true);

  toggleEditor() {
    this.isEditorVisible.set(!this.isEditorVisible());
  }
  
  addNotification() {
      this.notifications.add("Coucou", "Some text");
  }
}

mount(MyApp, document.body, { plugins: [NotificationPlugin] });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment