When onWillPatch and onPatched Fire
These hooks fire on every re-render — whether caused by state change, new props from the parent, or a forced re-render. They do not fire on the initial mount (that is onMounted's territory).
Trigger: state mutation or new props
│
├─ onWillUpdateProps(nextProps) ← only when props change (async, awaited)
│
├─ onWillPatch() ← sync, fires before virtual DOM diff
│ read current DOM here: scroll position, focus, measurements
│
├─ [virtual DOM diff + DOM patch] ← OWL applies minimal DOM changes
│
└─ onPatched() ← sync, fires after DOM is updated
react to new DOM: restore scroll, update external lib, measure
onWillPatch and onPatched run synchronously every time the component re-renders. Any expensive DOM query or computation here runs on every keystroke if your component updates frequently. Keep logic minimal — read one value, store it, restore it. Never make network calls in these hooks.
onWillPatch – Snapshot Before Update
The primary use case for onWillPatch is to capture DOM state that would be lost during the patch. The most common example is scroll position.
import { Component, xml, useState, onWillPatch, onPatched, useRef } from "@odoo/owl";
class MessageList extends Component {
static template = xml`
<div class="message-list" t-ref="list">
<div t-foreach="props.messages" t-as="msg" t-key="msg.id"
class="message" t-esc="msg.text"/>
</div>
`;
static props = {
messages: { type: Array },
};
listRef = useRef("list");
// Store scroll state between renders
_scrollTop = 0;
_scrollHeight = 0;
setup() {
onWillPatch(() => {
// Capture scroll position before the DOM is patched
const el = this.listRef.el;
if (el) {
this._scrollTop = el.scrollTop;
this._scrollHeight = el.scrollHeight;
}
});
onPatched(() => {
const el = this.listRef.el;
if (!el) return;
const newScrollHeight = el.scrollHeight;
const isAtBottom = this._scrollTop + el.clientHeight >= this._scrollHeight - 5;
if (isAtBottom) {
// User was at the bottom — keep them at the bottom after new messages added
el.scrollTop = newScrollHeight;
} else {
// User scrolled up — preserve their position
el.scrollTop = this._scrollTop + (newScrollHeight - this._scrollHeight);
}
});
}
}
onPatched – React to Updated DOM
onPatched fires after every re-render. At this point the DOM fully reflects the new component state. Use it to:
- Restore scroll position (paired with
onWillPatchsnapshot) - Notify a third-party library that the DOM changed
- Measure new DOM dimensions after a layout change
- Trigger animations when specific state changes
class LiveChart extends Component {
static template = xml`
<div>
<canvas t-ref="canvas"></canvas>
<button t-on-click="addDataPoint">Add point</button>
</div>
`;
canvasRef = useRef("canvas");
chart = null;
state = useState({
data: [10, 25, 18, 40, 32],
});
setup() {
onMounted(() => {
// Initialize chart after first render
this.chart = new Chart(this.canvasRef.el, {
type: "line",
data: { datasets: [{ data: this.state.data }] },
});
});
onPatched(() => {
// After every state update, sync the chart with new data
if (this.chart) {
this.chart.data.datasets[0].data = [...this.state.data];
this.chart.update(); // Chart.js re-draws
}
});
onWillUnmount(() => {
this.chart?.destroy();
});
}
addDataPoint() {
this.state.data.push(Math.floor(Math.random() * 60));
// ↑ triggers re-render → onPatched fires → chart.update() called
}
}
Triggering Animations on State Change
A common pattern: add a CSS class to an element in onPatched to trigger an animation, then remove it after the animation ends.
class PriceDisplay extends Component {
static template = xml`
<span t-ref="price" t-esc="props.price"/>
`;
static props = { price: { type: Number } };
priceRef = useRef("price");
_prevPrice = null;
setup() {
onWillPatch(() => {
this._prevPrice = this.props.price; // save current before update
});
onPatched(() => {
const el = this.priceRef.el;
if (!el || this.props.price === this._prevPrice) return;
// Price changed — trigger flash animation
const cls = this.props.price > this._prevPrice ? "flash-green" : "flash-red";
el.classList.add(cls);
el.addEventListener("animationend", () => el.classList.remove(cls), { once: true });
});
}
}
onWillPatch vs onPatched – Summary Table
| Feature | onWillPatch | onPatched |
|---|---|---|
| Fires when | Before DOM patch (after state/prop change) | After DOM patch (DOM fully updated) |
| DOM reflects | Old state — last render's DOM | New state — current render's DOM |
| Async? | ❌ Synchronous only | ❌ Synchronous only |
| Fires on initial mount? | ❌ No | ❌ No (use onMounted) |
| Primary use | Snapshot DOM state before it changes | Restore or react after DOM update |
| Common pattern | Read scroll position, element dimensions | Restore scroll, update chart, trigger animation |
Use onMounted for work that should happen once when the component first appears. Use onPatched for work that should repeat after every update. If you need both (initial setup + updates), register both hooks — the logic will naturally duplicate if they share code, so extract it into a shared method.
📋 Summary
onWillPatchfires synchronously before every DOM patch (re-render after initial mount). DOM still reflects the previous render.onPatchedfires synchronously after every DOM patch. DOM fully reflects the new state.- Neither hook fires on the initial mount — use
onMountedfor that. - Both are synchronous — no async work, no network calls. Keep them fast.
- Classic pattern:
onWillPatchcaptures scroll position →onPatchedrestores it based on whether the user was at the bottom. - Use
onPatchedto sync third-party libraries (charts, datepickers) after OWL updates the DOM. - Use plain instance properties (not
useState) to store snapshot values betweenonWillPatchandonPatched— they must not trigger a re-render.
🏋️ Exercise
Build an InfiniteScrollList component using the patch hooks:
- State: array of items (start with 10), a
loadingMoreflag. - In
onWillPatch: recordthis._scrollTopandthis._scrollHeightfrom the scroll container. - In
onPatched: whenloadingMorejust becamefalse(compare with a previous flag), restore scroll so the user does not jump — use the formulascrollTop = newScrollHeight - oldScrollHeight + oldScrollTop. - Add a "Load More" button that sets
state.loadingMore = true, waits 300ms, pushes 10 more items, and setsstate.loadingMore = false. - Add a scroll event listener to automatically trigger load-more when the user scrolls near the bottom (within 50px).
Frequently Asked Questions
Technically yes, but be very careful — mutating useState state in onPatched schedules another re-render, which fires onWillPatch and onPatched again. If your condition is not strict enough, this creates an infinite update loop. If you must update state in onPatched, use a guard flag that prevents repeated updates: only mutate when a specific condition changes, and ensure the mutation resolves that condition so it does not trigger again.
OWL does not have explicit watchers or computed properties in the Vue sense. onPatched is the closest equivalent for DOM side effects after state changes. For purely derived data (not DOM-related), use JavaScript getters instead — they are re-evaluated every render and are simpler. Use onPatched only when you need to interact with the DOM after an update.
No — lifecycle hooks fire on the component whose state or props changed, not on ancestors. If only a child component updates, the child's onWillPatch and onPatched fire but the parent's do not. OWL's component isolation means each component's lifecycle is independent. This is also why OWL is efficient — updating one deep component does not re-render or trigger hooks on all ancestors.