Fundamentals
Answer: OWL (Odoo Web Library) is a class-based JavaScript UI framework developed by Odoo SA. It powers the entire Odoo web client (backend, website, POS, mobile). OWL uses QWeb templates (XML-based), a Proxy-based reactivity system, and a fiber-based concurrent renderer. Unlike React or Vue, it is purpose-built for Odoo — its API aligns with Odoo's patterns (class inheritance, Python-style naming, registry system). As of Odoo 15, the entire web client was rewritten in OWL. All custom Odoo frontend code is expected to use OWL.
Answer: A class that extends Component and a static template property using the xml`` tagged template literal. Example: class Greet extends Component { static template = xml`<div>Hello</div>`; }. The xml`` tag is not optional — it compiles and registers the QWeb template; a plain string is not recognized by OWL's template engine.
Answer: static components = { ChildName } registers which component classes can be used in the template. OWL uses this map to look up the class when it encounters a PascalCase tag in the template. Without registration, OWL throws "Cannot find the definition of component 'ChildName'". Every component used in a template must be registered — this applies at every level, not just the root.
Answer: OWL's reactivity uses JavaScript Proxy. useState(obj) wraps the object in a Proxy that intercepts every property set, array mutation, and delete. When a mutation is detected, OWL schedules a microtask re-render for the owning component. Multiple mutations in the same synchronous call are batched — they produce one render. The Proxy is deep: nested objects and arrays inside the state are also wrapped when first accessed.
Answer: t-esc HTML-escapes the value before inserting it into the DOM — it prevents XSS by converting characters like < and > to entities. t-raw inserts the value as raw HTML without escaping. Use t-esc for all user-generated or external content. Use t-raw only for trusted, sanitized HTML you generated yourself.
Lifecycle
Answer: setup() is the designated initialization method called by OWL during component construction. It is where all lifecycle hooks (onMounted, onWillStart, etc.) and service injections (useService) must be called. It was introduced to enable custom hooks — composable functions that call OWL hooks internally. In OWL 1, lifecycle was via class method overrides (mounted(), willStart()) which could not be extracted and shared. OWL 2's setup() hooks are composable: multiple registrations of the same hook all fire, enabling hook libraries and reusable behaviors.
Answer: onWillStart fires before the first render and OWL awaits it — use it for async data fetching that the template depends on. The component does not render until it resolves. onMounted fires after the first render and DOM insertion — the DOM is available. OWL does not await onMounted. Use onMounted for DOM work: focus, scroll, third-party library init, global event listeners. Rule of thumb: data the template needs → onWillStart. DOM interaction → onMounted.
Answer: onWillUpdateProps(nextProps) fires when the parent passes new props, before the re-render that new props trigger. OWL awaits it. The nextProps argument contains the incoming new props; this.props still has the current (old) values. This lets you compare: if (nextProps.id !== this.props.id) await this.fetchRecord(nextProps.id). It is the correct place to re-fetch data when a key prop (like a record ID) changes.
Answer: Tear down everything created in onMounted: clear timers (clearInterval/clearTimeout), remove global event listeners (window.removeEventListener — must pass the same reference), disconnect observers (ResizeObserver.disconnect()), close WebSocket connections, and unsubscribe from any pub/sub channels. Forgetting cleanup causes memory leaks and ghost callbacks that fire after the component is gone, producing subtle bugs in long-running Odoo sessions.
State & Props
Answer: name="Alice" evaluates the JavaScript variable Alice (likely undefined). name="'Alice'" is a string literal because the inner single quotes make it a JavaScript string expression. Every prop attribute value in a QWeb template is a JavaScript expression — not a raw string. Always wrap string constants in inner single quotes: label="'Click me'".
Answer: Props are owned by the parent. Mutating them in the child desynchronizes the data — the parent's state does not reflect the change, so the next parent re-render will overwrite the mutation. OWL freezes this.props in dev mode to catch this. The correct pattern is one-way data flow: child notifies the parent via a callback prop (props.onChange(newValue)), parent updates its state, and the correct value flows back down as a new prop.
Answer: Use mutating array methods directly on the Proxy: this.state.items.push(x), this.state.items.splice(idx, 1), this.state.items.sort(fn). OWL intercepts these methods and triggers one re-render. For non-mutating operations (filter, map), assign the result back: this.state.items = this.state.items.filter(fn). Since both the filter and the assignment happen synchronously before the microtask flushes, they are still batched into one render.
Templates & Directives
Answer: t-key gives each list item a stable identity for OWL's vdom diffing. Without it, OWL falls back to using the loop index — items reuse DOM nodes by position. When items are inserted, deleted, or reordered, the wrong DOM node is reused, causing mismatched content, incorrect event handlers, and component state attached to the wrong item. Always use a stable unique value from the data (a database ID) rather than the loop index when items can change order.
Answer: t-if="false" removes the element from the DOM entirely — it does not exist, child components are unmounted, lifecycle hooks run. display: none hides but keeps the element in the DOM — child components remain mounted. Choose t-if when the element should not consume resources when hidden (expensive components, rarely shown content). Choose CSS hiding when: the element toggles frequently, you need to preserve child state (a draft text area), or the toggle cost is high relative to keep-alive cost.
Answer: t-model updates state on the input event — every keystroke. t-model.lazy updates state on the change event — when the field loses focus or a selection-type input commits. Use .lazy when live updates are expensive (heavy computed values, API calls) or when you want to validate only after the user finishes typing rather than per-keystroke.
Advanced
Answer: A custom hook is a plain function (by convention named use*) that calls OWL hook functions (useState, onMounted, etc.) internally. It must be called synchronously inside setup() because OWL's "current component" context — used to register hooks to the correct component — is only active during setup. Calling a hook outside setup throws "No component is being set up." Custom hooks enable extracting reusable lifecycle + state logic without creating new components.
Answer: Slots allow a parent to inject template content into a child's designated placeholders (<t t-slot="slotName"/>). Default slot: content between child's opening/closing tags goes into t-slot="default". Named slots: parent provides content with <t t-set-slot="name">...</t>; child renders it with <t t-slot="name"/>. Content placed inside <t t-slot="name">fallback</t> in the child is shown when the parent provides no content for that slot. Slot content is evaluated in the parent's scope, not the child's.
Answer: OWL renders components via fibers — lightweight units of render work. When a component needs to re-render, OWL creates a fiber. If the render requires awaiting an async hook (onWillStart, onWillUpdateProps), the fiber suspends. If new state arrives while the fiber is suspended, OWL cancels the old fiber and starts a new one with the latest state — preventing stale renders. Multiple synchronous mutations within one call stack are batched via microtasks into a single render. Mutations across an await are NOT batched — each produces a separate render.
Answer: Use AbortController to cancel in-flight requests: before starting a new fetch, call this._abortController?.abort() to cancel the previous one. Create a new controller for each request. In the catch block, check for err.name === "AbortError" and ignore it. For non-fetch async (where AbortController is not applicable), use a request-ID counter — increment it before each call, capture the current value, and after the await, check if the captured value still equals the current counter. If not, the call is stale — return without updating state.
Answer: /** @odoo-module **/ is a pragma comment at the top of every Odoo JS file that uses ES module syntax. Odoo's asset bundler uses it to switch the file into ES module mode, enabling import/export syntax. Without it, the bundler treats the file as a classic script and import statements cause syntax errors. It must be the very first line of the file, before any import statement.
Answer: In JavaScript: registry.category("fields").add("widget_name", { component: MyWidget, supportedTypes: ["char"] }). The component must accept standardFieldProps (record, name, readonly, etc.). In XML: <field name="my_field" widget="widget_name"/>. The JS file must be listed in __manifest__.py under web.assets_backend. Odoo looks up the widget by name when rendering the view and instantiates your component with the field record context.
Answer: useEnv() returns the raw OWL environment object — available in all OWL apps. In Odoo, the environment contains an env.services object populated by the framework. useService("name") is an Odoo-specific utility from @web/core/utils/hooks that reads env.services.name — a convenience wrapper over useEnv() that also adds dev-mode checks. In standalone OWL apps (no Odoo), use useEnv() directly and populate the env at mount() time. In Odoo modules, always use useService().
Answer: onError(callback) makes a component an error boundary — it catches errors thrown by any descendant component (render errors, lifecycle hook errors, uncaught async errors). The callback receives the error object. The error boundary component can then render fallback UI via its own state. Without an error boundary, an error propagates up and the entire app may crash. Best practice: wrap each independent widget section in its own boundary so failures are isolated. Use t-key increment on retry to force a fresh remount of the failed child.
Answer: Use this.orm.call(model, methodName, [ids], kwargs). The first positional argument must always be a list of record IDs (for instance methods) or an empty list (for @api.model classmethods). Additional keyword arguments go in the fourth parameter. Server-side errors arrive as rejected Promises — check err.data.name for the exception class and err.data.message for the human-readable text from Python's UserError.
Answer: onSave.bind="handleSave" passes the method pre-bound to the parent component's this. Without .bind, passing onSave="handleSave" passes the unbound method reference — when the child calls it, this inside the method is undefined or the wrong context. The .bind suffix is OWL's declarative equivalent of calling this.handleSave.bind(this). Always use it when passing methods as callback props.
Practical Scenarios
Answer: Write a useDebounce(fn, delay) custom hook that uses onWillUnmount to clear the pending timer. In the component, call it in setup(): this.debouncedSearch = useDebounce(this.search.bind(this), 300). Bind the input with t-model="state.query" and t-on-input="debouncedSearch". The search fires only after 300ms of inactivity. Combine with AbortController in the search method to cancel any in-flight request when a new one starts (race condition prevention).
Answer: Use t-key on the child component with a value that changes when you want a reset: <UserForm t-key="state.selectedUserId" userId="state.selectedUserId"/>. When state.selectedUserId changes, OWL treats it as a new component instance — unmounts the old one (lifecycle hooks run) and mounts a fresh one (onWillStart re-runs, all local state resets). This is simpler and more reliable than trying to reset state from inside the component.
Answer: Domains are arrays of condition triples: [fieldName, operator, value]. Conditions are AND'd by default. Use "|" prefix for OR. Example: fetch confirmed sale orders from today or yesterday for the current user: [["user_id", "=", this.user.userId], "&", "|", ["date_order", "=", today], ["date_order", "=", yesterday], ["state", "=", "sale"]]. Common operators: "=", "!=", "ilike" (case-insensitive contains), "in", ">=", "<=".
Answer: Use the onWillPatch / onPatched pair. In onWillPatch, read and store el.scrollTop and el.scrollHeight. In onPatched, check if the user was at the bottom (scrollTop + clientHeight ≥ scrollHeight - threshold). If yes, set el.scrollTop = el.scrollHeight (auto-scroll to bottom). If no, set el.scrollTop = oldScrollTop + (newScrollHeight - oldScrollHeight) to preserve position relative to content above the viewport.
Answer: It throws: "No component is being set up." OWL maintains a global "current component" reference that is only set during setup()'s synchronous execution. Hook functions (useState, onMounted, useRef, etc.) read this reference to know which component to register against. Calling them in an event handler (after setup is done), an async callback, or a plain method has no active "current component" — hence the error. Always call hooks synchronously in setup().
📋 Study Checklist
- Component anatomy:
extends Component,xml``,static components,static props,static defaultProps - Lifecycle order: setup → onWillStart → render → onMounted → (update: onWillUpdateProps → onWillPatch → render → onPatched) → onWillUnmount
- Reactivity: Proxy-based, direct mutation, batching, deep nesting, array methods
- Template directives: t-if/elif/else, t-foreach/as/key, t-model + modifiers, t-att-class, t-slot
- Custom hooks: must be in setup(), composable, return reactive values
- Error handling: onError boundaries, try/catch in hooks, async error guards
- Odoo: @odoo-module, registry.category("fields").add(), useService(), orm.call()