Technical

How OWL Changed Odoo Frontend Development

Published on 07/29/2026
How OWL Changed Odoo Frontend Development

1. Introduction
Why OWL Exists and Why It Keeps Changing

If you've built Odoo modules for a while, you've watched the frontend quietly transform. The old jQuery-heavy, widget-wiring days are mostly behind us. What replaced them is OWL Odoo Web Library a reactive component framework that genuinely competes with React and Vue.

OWL landed with Odoo 14 in 2020, and the shift was significant. Instead of manually wrestling with the DOM and chasing legacy widget events, you could write clean, self-contained components and let the framework handle the heavy lifting.

But it didn't stop there. OWL 2 (Odoo 16/17) introduced hooks and delivered serious performance improvements. OWL 3 is still taking shape, but it's already promising signals-based reactivity and a plugin architecture that could change how we build Odoo UIs entirely.

This guide walks through all three versions what changed, why, and what it actually means for your day-to-day work. Think of it less as documentation and more as a developer-to-developer conversation about a framework that's still finding its ceiling.

Quick Version Summary

OWL 1 → Component classes with lifecycle methods (Odoo 14–15)  |  OWL 2 → Hooks-based API, 20× faster rendering (Odoo 16–17)  |  OWL 3 → Signals, plugins, fine-grained reactivity (Planned, 2026+)

2. Internal Workflow
What Happens When OWL Renders

Before diving into version differences, it's worth understanding what OWL is actually doing. At its core, it handles three things: compiling templates into DOM, managing reactive state, and updating the UI efficiently when data changes. Each version tackles this a bit differently.

2.1 OWL 1 Parse, Compile, Render

When your component loads, OWL's QWeb engine compiles your XML templates into a virtual DOM. That gets mounted into the browser, and the UI appears. When something changes a button click, a form submission state updates, OWL queues a re-render, diffs the result against the current DOM, and applies only what's necessary. Familiar concept if you've used React; just living entirely inside the Odoo ecosystem.

2.2 OWL 2 Hooks Take the Wheel

The rendering pipeline stays largely the same, but how components are initialized changes completely. Out go lifecycle overrides like mounted() and willStart(). In comes a single setup() function where you register everything as hooks. The framework owns the lifecycle; your code just plugs in where it needs to.

Key Concept

This isn't just cleaner it makes components genuinely easier to test and compose. It also lets OWL batch and defer re-renders more precisely, which explains the noticeable performance jump between versions.

2.3 OWL 3 Signals Change the Game (Proposed)

OWL 3 wants to ditch the "state changed → re-render component" model entirely. Instead, signals lightweight reactive primitives track exactly which parts of the UI depend on them. Change a signal, and only the specific nodes reading it update. Nothing else touches.

Clarification

It's the same approach Vue 3 and Svelte have proven out, and for complex, data-heavy UIs, the performance difference can be substantial. Less guesswork, more precision.

3. Architecture
How Components Are Structured Across Versions

The clearest way to see OWL's evolution is through the component lifecycle: how it starts, fetches data, updates, and cleans up.

3.1 OWL 1 Class Methods for Everything

Components extended owl.Component and used override methods for each lifecycle stage. willStart() for async data, mounted() after DOM insertion, willUnmount() for cleanup. Reactive state came from useState() mutate the returned proxy, and OWL schedules a re-render automatically.

It worked, but logic scattered across multiple methods made components harder to follow as they grew.

3.2 OWL 2 One Function to Rule Them All

OWL 2 pulls everything into setup(). Instead of overriding methods, you register hooks onWillStart(), onMounted(), onWillUnmount() as plain function calls in one place. Read a component top to bottom, understand it completely. No jumping around.

Two other quality-of-life upgrades came along for the ride. Props moved from a loose string array to a typed schema, making components self-documenting and catching bad prop usage early. Services like RPC and notifications became accessible through useService(), replacing global imports with explicit, testable dependencies. It's a smaller surface area with significantly fewer surprises.

Concept OWL 1 OWL 2 OWL 3 (Planned)
Component base Extends owl.Component Extends Component (same) Component class (signals replace state)
Initialization Override willStart(), mounted() Hooks inside setup() setup() with signals/effects
State system useState() proxy useState() proxy (optimized) signal() + computed()
Lifecycle hooks Class method overrides onMounted(), onWillStart(), etc. Updated hook set (some removed)
Props declaration Static array: ['name', 'id'] Object schema: { name: {type: String} } Enhanced schema with function types
Service access useService('rpc') via hooks useService('rpc') same Plugin API: usePlugin('rpc')
Template marker owl="1" required Not needed Not needed
Environment (env) Global env object env available Replaced by plugin system

4. Real Examples
Seeing the Difference in Action

Abstract comparisons only go so far. The clearest way to understand the shift from OWL 1 to OWL 2 (and to OWL 3) is to look at the same component written across versions.

4.1 Example 1: A Simple Counter

Let's start with the most basic example a button that increments a counter.

OWL 1 Lifecycle as class methods
class Counter extends owl.Component {
  setup() {
    this.state = useState({ count: 0 });
  }

  increment() {
    this.state.count++;
  }
}

Counter.template = xml`
  <button t-on-click="increment">
    Count: <t t-esc="state.count"/>
  </button>`;
OWL 2 Same logic, hooks added
class Counter2 extends Component {
  static props = {
    start: { type: Number }
  };

  setup() {
    this.state = useState({
      count: this.props.start || 0
    });

    onMounted(() => console.log('Counter ready'));
  }

  increment() {
    this.state.count++;
  }
}
OWL 3 (Proposed) Signals replace useState
import { signal, onMounted } from "@odoo/owl";

class Counter3 extends Component {

  setup() {
    const count = signal(0);

    onMounted(() => console.log('Ready'));

    return { count };
  }

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

}

Notice the progression: OWL 1 uses simple property assignment inside setup(). OWL 2 adds proper props validation and lifecycle hooks. OWL 3 replaces the proxy-based useState() with a signal a leaner, more targeted reactive primitive.

4.2 Example 2: Fetching Data from the Odoo Backend

A more realistic scenario: a component that loads records from Odoo via RPC and renders them in a list.

OWL 1 willStart() handles async fetch
class NameList extends Component {

  setup() {
    this.state = {
      names: [],
      loading: true
    };

    this.rpc = owl.services.rpc;
  }

  async willStart() {

    const res = await this.rpc(
      "/web/dataset/call_kw",
      {
        model: "res.partner",
        method: "search_read",
        args: [[], ['name'], 5],
        kwargs: {},
      }
    );

    this.state.names = res.map(r => r.name);
    this.state.loading = false;
  }

}
OWL 2 Same result, via onWillStart() hook
import { useService } from "@web/core/utils/hooks";

class NameList2 extends Component {

  setup() {

    this.state = {
      names: [],
      loading: true
    };

    const rpc = useService("rpc");

    onWillStart(async () => {

      const res = await rpc(
        "/web/dataset/call_kw",
        { ... }
      );

      this.state.names = res.map(r => r.name);
      this.state.loading = false;

    });
  }

}

The logic is identical only the structure changes. The OWL 2 version is arguably easier to read because everything related to the component's behaviour lives inside setup(), rather than being split across multiple methods. For large components, this difference really adds up.

5. Performance
Where OWL 2 Made the Biggest Leap

This is where the upgrade gets hard to ignore. Odoo's own team reported backend views loading up to 20× faster after moving to OWL 2. One view that used to take ~800ms was rendering in ~40ms. That's not a benchmark number that's a difference you actually feel while using the app.

The gains came from smarter batching. Instead of re-rendering on every individual state mutation, OWL 2 groups changes and applies one pass. Fetch data, update five state properties, get one re-render. OWL 1 would've fired five. That adds up fast on complex pages.

Fewer redundant RPC calls and cleaner lifecycle management helped too pages loaded with dozens of charts and dynamic widgets started feeling genuinely snappy.

OWL 3 wants to go further. Signals mean only the exact DOM nodes tied to a changed value get updated nothing else even looks at it. For real-time dashboards or collaborative views with constant data churn, that surgical precision could be another meaningful leap. That said, for most Odoo projects, OWL 2 is already plenty fast. Unless you're managing thousands of simultaneous reactive bindings, you're unlikely to hit a wall.

OWL 2 in Numbers

20× faster page loads in backend views  |  ~800ms → ~40ms for large view rendering  |  Fewer unnecessary RPC calls  |  Smooth rendering even with 1,000+ list items

Best Practices

6. Writing OWL Components That Age Well

Guidelines to keep OWL components predictable, testable, and ready for future upgrades.

Do

Recommended

Don't

Avoid
Keep components small

One job per component. Break up anything fetching data, managing multiple state slices, and rendering a complex template all at once.

Overload a single component

Stacking unrelated responsibilities into one component makes it fragile and hard to test.

Treat setup() as the source of truth

State, services, lifecycle hooks all of it lives here. If something drifts outside setup(), it probably belongs in a utility function or service.

Call hooks conditionally

Hooks must run in the same order every render. Wrapping onMounted() in an if statement causes unpredictable behaviour.

Validate your props

A typed prop schema is live documentation, telling the framework and your teammates exactly what a component expects.

Mutate props directly

Direct prop mutation breaks one-way data flow. Copy the value into local state inside setup() instead.

Always clean up

Event listeners, chart libraries, bus subscriptions anything set up should be torn down in onWillUnmount().

Lean on the global env object

Write OWL 2 code with OWL 3 in mind keep state flat, prefer computed getters, and avoid depending on env directly.

7. Common Mistakes
What Trips Developers Up

Migrating Lifecycle Methods but Missing One

The most frequent OWL 1→2 migration bug is converting mounted() to onMounted() but forgetting patched() or willUnmount(). The old methods still exist silently they just don't fire. Cleanup or chart-update logic simply stops working, and it's not always obvious why.

Calling Hooks Conditionally

OWL 2 hooks follow the same rules as React hooks: they must be called in the same order on every render. Hooks must always be called unconditionally from the top level of setup().

Directly Mutating Objects Inside useState

Writing this.state.items.push(newItem) looks like it should work, but deeply nested mutations can slip past OWL's reactivity proxy without triggering a re-render. Either reassign the array or use OWL's reactive utilities.

Confusing @odoo/owl Imports with Global owl

OWL 2 uses ES module imports: import { Component, useState } from '@odoo/owl'. Using the legacy global owl object in a mixed codebase can cause version conflicts that are surprisingly hard to debug.

Migrating to OWL 3 Too Early

OWL 3 is still experimental. Using speculative OWL 3 patterns in production OWL 2 code introduces instability and makes your codebase diverge from what Odoo officially supports. Prepare by writing clean, future-compatible OWL 2 code, and migrate to OWL 3 once it ships with a stable release.

8. Migration
Moving Between OWL Versions

8.1 OWL 1 → OWL 2: Manageable with a Checklist

The OWL 1 to OWL 2 migration is largely mechanical. The component structure stays the same you're mostly renaming things and reorganizing where code lives.

What to Change OWL 1 Pattern OWL 2 Pattern
Lifecycle: data fetch async willStart() { ... } onWillStart(async () => { ... }) inside setup()
Lifecycle: after mount mounted() { ... } onMounted(() => { ... }) inside setup()
Lifecycle: after update patched() { ... } onPatched(() => { ... }) inside setup()
Lifecycle: cleanup willUnmount() { ... } onWillUnmount(() => { ... }) inside setup()
Props declaration static props = ['title', 'count'] static props = { title: {type:String}, count: {type:Number} }
RPC service owl.services.rpc const rpc = useService('rpc')
Template tag owl="1" on <templates> Not needed in OWL 2+
Import style const { Component } = owl.hooks import { Component } from '@odoo/owl'

After converting lifecycle methods to hooks and updating the props schema, test each component individually before deploying. The Odoo 17/18 environment is your best validator.

8.2 OWL 2 → OWL 3: A More Significant Rewrite

The OWL 3 migration will be more involved because the reactivity model changes at a fundamental level. useState() is deprecated in favour of signal(). The env object goes away, replaced by a plugin system. Some template directives are renamed or removed entirely.

The key migration steps, based on current OWL 3 draft documentation:

  • Replace useState({ ... }) with individual signal() calls for each reactive value
  • Use computed() for values derived from other signals, rather than JavaScript getters
  • Migrate from useService('rpc') to the plugin API equivalent (usePlugin('rpc'))
  • Update templates: t-slot becomes t-call-slot, and t-portal is removed
  • Fix t-call usage: t-set variables inside t-call blocks must be passed as attributes
  • Remove reliance on the global env object in all components
OWL 3 Migration Advice

Don't rush the OWL 3 migration. Wait for the stable release and official migration tooling. In the meantime, writing clean OWL 2 code with flat state and minimal env usage will make the eventual upgrade much less painful.

9. Side-by-Side Comparison

Feature OWL 1 (Odoo 14–15) OWL 2 (Odoo 16–17) OWL 3 (Planned)
Release status Stable, legacy Stable, current Experimental / in design
Odoo release Odoo 14 (Oct 2020) Odoo 16 (Oct 2022), 17 (Nov 2023) Post-2025 (est. 2026+)
API paradigm Class lifecycle methods Hooks inside setup() Signals + effects
State management useState() proxy useState() proxy (optimized) signal() + computed()
Props system String array Object with type schema Enhanced object schema
Services useService() hooks useService() hooks Plugin API (usePlugin())
Template syntax QWeb XML, owl="1" QWeb XML, no owl="1" Updated QWeb, some tags renamed
Performance Good for small–medium UIs Up to 20× faster than OWL 1 Expected: finer-grained, faster
Backward compatibility Original version Backward compatible with OWL 1 Breaking migration required
Best for Legacy modules, Odoo 14–15 All current development Future large-scale apps

10. Conclusion
OWL's Journey and What It Means for You

OWL's evolution tells the story of Odoo's frontend growing up. Version 1 was a genuine improvement over the old widget system declarative, reactive, and component-based. But it was still learning. Version 2 took all that potential and delivered it at scale: hooks made code more composable, performance improved dramatically, and the development experience began to feel genuinely modern.

Version 3, once it arrives, will push the boundary further. Signal-based reactivity is a more precise and efficient model than proxy-based state, and a plugin architecture will make it far easier to build modular, testable Odoo front-ends. But it's still on the horizon treat it as something to prepare for, not something to adopt today.

For most Odoo developers right now, the practical takeaway is this: if you're still on OWL 1, the migration to OWL 2 is straightforward and well worth doing. The lifecycle hook rename is mostly mechanical, and the performance and maintainability gains are real. If you're already on OWL 2, you're in a good place write clean, hook-based components, keep props validated, and let the framework do what it does well. The Odoo frontend has never been more capable. Understanding how it evolved helps you write code that takes full advantage of where it is today and stays ready for where it's heading.

Quick Reference

Everything you should remember about OWL's evolution at a glance.

Golden Rule

Write clean OWL 2 code with OWL 3 in mind.

Keep setup() as the single source of truth, validate props, avoid the global env object, and prefer computed getters over manually derived state these habits map cleanly onto signal-based patterns.

setup()

Register state, services, and lifecycle hooks all in one place.

Performance

OWL 2 batches state changes into a single re-render pass.

Services

Access RPC and notifications explicitly via useService().

Signals

OWL 3's fine-grained reactivity, replacing useState() proxies.

Props

Use a typed object schema instead of a string array.

Cleanup

Tear down listeners and subscriptions in onWillUnmount().

Frequently Asked Questions

Common questions developers ask about OWL in Odoo.

What is OWL in Odoo?

OWL Odoo Web Library is Odoo's reactive component framework, comparable to React or Vue. It handles template compilation, reactive state, and efficient DOM updates, and has powered Odoo's frontend since version 14.

What's the main difference between OWL 1 and OWL 2?

OWL 1 used class method overrides like willStart() and mounted(). OWL 2 replaces these with hooks called inside a single setup() function, and adds typed props and useService() for cleaner, more testable components.

How much faster is OWL 2 than OWL 1?

Odoo reported backend views loading up to 20× faster after the OWL 2 migration, with some views dropping from ~800ms to ~40ms, mainly thanks to smarter batching of state updates.

~800ms → ~40ms in large view rendering

Should I start using OWL 3 now?

No. OWL 3 is still experimental and not officially supported. Write clean OWL 2 code flat state, minimal env usage, computed getters so the eventual migration to signals is straightforward once OWL 3 ships stable.

Global Reach

Consult Expertise

Engage with our core laboratory engineering leads to optimize your infrastructural frameworks.