Advertisement
🦉 Lifecycle

OWL JS onWillPatch & onPatched – Before and After Re-render

onWillPatch and onPatched are the update-phase lifecycle hooks — they fire around every DOM patch (re-render) after the initial mount. onWillPatch lets you read the current DOM state before it changes. onPatched lets you react to the new DOM state after it has been updated. These two hooks are less frequently needed than onMounted or onWillStart, but they are essential for specific patterns like scroll position preservation, DOM diffing side effects, and syncing with third-party libraries on update.

⏱️ 16 min read 🎯 Intermediate 📅 Updated 2026 👁️ Lesson 4 of 5

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).

Text – update phase timeline
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
⚠️
These hooks fire on EVERY update — keep them fast

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.

JavaScript – scroll preservation with willPatch + patched
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 onWillPatch snapshot)
  • Notify a third-party library that the DOM changed
  • Measure new DOM dimensions after a layout change
  • Trigger animations when specific state changes
JavaScript – update a third-party chart on patch
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
  }
}
Advertisement

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.

JavaScript – CSS flash animation on value change
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

FeatureonWillPatchonPatched
Fires whenBefore DOM patch (after state/prop change)After DOM patch (DOM fully updated)
DOM reflectsOld state — last render's DOMNew state — current render's DOM
Async?❌ Synchronous only❌ Synchronous only
Fires on initial mount?❌ No❌ No (use onMounted)
Primary useSnapshot DOM state before it changesRestore or react after DOM update
Common patternRead scroll position, element dimensionsRestore scroll, update chart, trigger animation
ℹ️
onPatched vs onMounted for DOM work

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.

onWillPatch and onPatched: Reacting to Re-renders

The patch hooks fire around a re-render (a "patch" of the DOM after state or props change) — as opposed to the mount hooks, which fire only once when the component is first added. Use them to react to updates.

HookFiresUse for
onWillPatchjust before the DOM updatescapture pre-update DOM state (e.g. scroll pos)
onPatchedjust after the DOM updatesread/adjust the updated DOM
onMountedonce, on first insertinitial DOM setup
setup() {
    let scroll = 0;
    onWillPatch(() => { scroll = this.ref.el.scrollTop; });  // remember before
    onPatched(() => { this.ref.el.scrollTop = scroll; });    // restore after
}

Mount vs patch — the key distinction: onMounted/onWillUnmount run once (component appears/disappears), while onWillPatch/onPatched run on every re-render after the first. So DOM work that must repeat whenever the view updates belongs in the patch hooks, not onMounted. The classic use case is the scroll-preservation pattern above: a re-render can reset scroll position, so you capture it in onWillPatch (before the DOM changes) and restore it in onPatched (after). Similar uses include re-measuring an element's size after content changes or re-syncing a third-party widget with new data. Caution — infinite loops: never change reactive state inside onPatched without a guard, because that triggers another render → another patch → another state change, looping forever. Only touch the DOM there, or gate state changes behind a condition. Rule reminder: like all hooks, register these synchronously inside setup(). Most components never need patch hooks — reach for them only when you must react to DOM updates imperatively; for reacting to prop changes, onWillUpdateProps is usually the better tool.

🏋️ Practical Exercise

Build an InfiniteScrollList component using the patch hooks:

  1. State: array of items (start with 10), a loadingMore flag.
  2. In onWillPatch: record this._scrollTop and this._scrollHeight from the scroll container.
  3. In onPatched: when loadingMore just became false (compare with a previous flag), restore scroll so the user does not jump — use the formula scrollTop = newScrollHeight - oldScrollHeight + oldScrollTop.
  4. Add a "Load More" button that sets state.loadingMore = true, waits 300ms, pushes 10 more items, and sets state.loadingMore = false.
  5. Add a scroll event listener to automatically trigger load-more when the user scrolls near the bottom (within 50px).

🔥 Challenge Exercise

Use onWillPatch and onPatched to run code around DOM updates (e.g. preserve scroll position across a re-render). Explain when patch hooks fire (on updates, not the initial mount) and a real use case.

📋 Summary

  • onWillPatch fires synchronously before every DOM patch (re-render after initial mount). DOM still reflects the previous render.
  • onPatched fires synchronously after every DOM patch. DOM fully reflects the new state.
  • Neither hook fires on the initial mount — use onMounted for that.
  • Both are synchronous — no async work, no network calls. Keep them fast.
  • Classic pattern: onWillPatch captures scroll position → onPatched restores it based on whether the user was at the bottom.
  • Use onPatched to sync third-party libraries (charts, datepickers) after OWL updates the DOM.
  • Use plain instance properties (not useState) to store snapshot values between onWillPatch and onPatched — they must not trigger a re-render.

Interview Questions

  • What are onWillPatch and onPatched?
  • When do patch hooks fire?
  • How do they differ from onMounted?
  • Give a use case for onPatched.
  • Do patch hooks run on the first render?

FAQ

Can I update state in onPatched? +

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.

What is the difference between onPatched and a watcher/computed on state? +

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.

Do onWillPatch and onPatched fire if only a child component's state changes? +

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.