Ad – 728×90
🦉 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
  }
}
Ad – 336×280

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.

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

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

Frequently Asked Questions

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.