Ad – 728×90
🦉 Lifecycle

OWL JS onWillStart & Async Initialization

onWillStart is OWL's only async pre-render hook — it fires before the component's first render and OWL waits for it to complete before showing anything. This makes it the correct place to fetch initial data, load resources, or perform any async work that the template depends on. Similarly, onWillUpdateProps lets you perform async work when the parent sends new props — before the re-render triggered by those new props. This lesson covers both hooks in depth, with loading state patterns, error handling, and common real-world examples.

⏱️ 18 min read 🎯 Intermediate 📅 Updated 2026 👁️ Lesson 3 of 5

onWillStart – Before the First Render

onWillStart is called once, after setup() completes and before the first render. OWL awaits its Promise — the component does not render until the hook resolves. Use it to fetch data that the template needs to display on first paint.

JavaScript – basic onWillStart
import { Component, xml, useState, onWillStart } from "@odoo/owl";

class UserProfile extends Component {
  static template = xml`
    <div>
      <t t-if="state.isLoading">Loading…</t>
      <t t-else="">
        <h2 t-esc="state.user.name"/>
        <p t-esc="state.user.email"/>
      </t>
    </div>
  `;

  static props = { userId: { type: Number } };

  state = useState({ user: null, isLoading: true });

  setup() {
    onWillStart(async () => {
      // OWL awaits this — component does not render until it resolves
      this.state.user      = await fetchUser(this.props.userId);
      this.state.isLoading = false;
    });
  }
}
ℹ️
Why not fetch in the constructor or setup() directly?

setup() is synchronous — you cannot await inside it. If you start a fetch in setup() without awaiting, the component renders immediately with empty data, then the fetch completes and a second render happens — causing a flash. onWillStart lets you await the fetch so the first render already has data. The component appears only after the data is ready.

Loading State Patterns

There are two main approaches to loading states with onWillStart:

Pattern 1: Explicit loading flag

JavaScript – explicit loading flag
class InvoiceList extends Component {
  static template = xml`
    <div>
      <div t-if="state.isLoading" class="loading-spinner">Loading invoices…</div>
      <div t-elif="state.error" class="error-box">
        <p t-esc="state.error"/>
        <button t-on-click="retry">Retry</button>
      </div>
      <t t-else="">
        <p t-if="state.invoices.length === 0">No invoices found.</p>
        <table t-if="state.invoices.length > 0">
          <tr t-foreach="state.invoices" t-as="inv" t-key="inv.id">
            <td t-esc="inv.name"/>
            <td t-esc="inv.amount"/>
          </tr>
        </table>
      </t>
    </div>
  `;

  state = useState({ invoices: [], isLoading: true, error: null });

  setup() {
    onWillStart(() => this.loadInvoices());
  }

  async loadInvoices() {
    try {
      this.state.isLoading = true;
      this.state.error     = null;
      this.state.invoices  = await fetchInvoices();
    } catch (err) {
      this.state.error = "Failed to load invoices: " + err.message;
    } finally {
      this.state.isLoading = false;
    }
  }

  retry() {
    this.loadInvoices();  // re-fetch on demand
  }
}

Pattern 2: Show nothing until data is ready

JavaScript – no render until data ready
class Dashboard extends Component {
  // Data starts as null — template guards against it
  state = useState({ metrics: null });

  setup() {
    onWillStart(async () => {
      // OWL waits here — nothing renders yet
      this.state.metrics = await fetchDashboardMetrics();
      // After this resolves, the component renders once with all data
    });
  }

  static template = xml`
    <div t-if="state.metrics">
      <p>Revenue: <t t-esc="state.metrics.revenue"/></p>
      <p>Orders: <t t-esc="state.metrics.orders"/></p>
    </div>
  `;
}
Ad – 336×280

onWillUpdateProps – React to New Props

onWillUpdateProps fires when the parent component passes new props. OWL awaits it before re-rendering. Use it when changing a prop (like an ID) requires fetching new data that the new render depends on.

JavaScript – onWillUpdateProps
import { Component, xml, useState, onWillStart, onWillUpdateProps } from "@odoo/owl";

class ProductDetail extends Component {
  static template = xml`
    <div>
      <div t-if="state.isLoading">Loading…</div>
      <div t-else="">
        <h2 t-esc="state.product.name"/>
        <p t-esc="state.product.description"/>
        <p>Price: $<t t-esc="state.product.price"/></p>
      </div>
    </div>
  `;

  static props = { productId: { type: Number } };

  state = useState({ product: null, isLoading: true });

  setup() {
    // Load data before first render
    onWillStart(() => this.loadProduct(this.props.productId));

    // When parent passes a different productId, load new data before re-render
    onWillUpdateProps(async (nextProps) => {
      if (nextProps.productId !== this.props.productId) {
        await this.loadProduct(nextProps.productId);
      }
    });
  }

  async loadProduct(id) {
    this.state.isLoading = true;
    try {
      this.state.product   = await fetchProduct(id);
    } finally {
      this.state.isLoading = false;
    }
  }
}
💡
nextProps vs this.props in onWillUpdateProps

Inside onWillUpdateProps(nextProps => ...), nextProps is the incoming new props object. this.props still has the current (old) props. This lets you compare old vs new: if (nextProps.id !== this.props.id). After the hook resolves, this.props is updated to nextProps and the re-render happens.

Real Odoo Example: ORM Data Loading

JavaScript – Odoo ORM in onWillStart
/** @odoo-module **/
import { Component, xml, useState, onWillStart, onWillUpdateProps } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";

class PartnerCard extends Component {
  static template = xml`
    <div class="partner-card">
      <t t-if="state.isLoading">
        <div class="o_loading"/>
      </t>
      <t t-else="">
        <h3 t-esc="state.partner.name"/>
        <span t-esc="state.partner.email"/>
        <span t-esc="state.partner.phone"/>
      </t>
    </div>
  `;

  static props = { partnerId: { type: Number } };

  state = useState({ partner: null, isLoading: true });

  setup() {
    this.orm = useService("orm");

    onWillStart(() => this.fetchPartner(this.props.partnerId));

    onWillUpdateProps(async (nextProps) => {
      if (nextProps.partnerId !== this.props.partnerId) {
        await this.fetchPartner(nextProps.partnerId);
      }
    });
  }

  async fetchPartner(id) {
    this.state.isLoading = true;
    const records = await this.orm.read("res.partner", [id], ["name", "email", "phone"]);
    this.state.partner   = records[0] || null;
    this.state.isLoading = false;
  }
}

onWillStart vs onWillUpdateProps vs onMounted

HookFires whenAsync?Use case
onWillStart Once, before first render ✅ Awaited Initial data fetch, auth check, resource loading
onWillUpdateProps Every time parent passes new props ✅ Awaited Re-fetch data when a key prop (like ID) changes
onMounted Once, after first render + DOM insert ❌ Not awaited DOM work, third-party init, listeners

📋 Summary

  • onWillStart fires before the first render and OWL awaits it — use it for async initialization that the template depends on.
  • onWillUpdateProps(nextProps => ...) fires before a re-render caused by new parent props — use it to fetch data based on incoming prop values.
  • Both hooks are async-safe — pass an async function or return a Promise and OWL will wait.
  • Inside onWillUpdateProps, compare nextProps.x to this.props.x to avoid unnecessary fetches when the relevant prop did not change.
  • Use a loading flag in state to show a spinner while async hooks are running — the component does render a loading state if onWillStart sets state before awaiting.
  • For Odoo modules, use useService("orm") and ORM methods in these hooks to load server data.

🏋️ Exercise

Build a SearchResults component that uses both onWillStart and onWillUpdateProps:

  1. Props: query (String) — the search term.
  2. In onWillStart, simulate an API call with await delay(500) (a helper that returns new Promise(r => setTimeout(r, 500))). Set fake results based on the query.
  3. In onWillUpdateProps, check if nextProps.query !== this.props.query. If so, set state.isLoading = true, simulate the fetch, and update results.
  4. Show a loading indicator while loading. Show "No results" when the results array is empty.
  5. In a parent App, add an input bound to state.query via t-model. Pass state.query to SearchResults. Observe: every time you type and the query changes, SearchResults shows a loading state then updates.

Frequently Asked Questions

Does the component show anything while onWillStart is running? +

No — OWL does not render the component at all until onWillStart resolves. The parent component renders but the child appears only after its onWillStart completes. If you want to show a loading spinner immediately, the loading state must be in the parent, not the child — or you can render a lightweight placeholder via t-if="state.data" only (hide the real content while loading, but still render a skeleton).

What happens if onWillStart throws or rejects? +

The error propagates up to the nearest ancestor with an onError hook registered. If no ancestor handles it, OWL treats it as an unhandled Promise rejection. The component that threw never renders. This is the right place to implement error boundaries — the parent catches the child's init failure and shows a fallback UI. Always wrap async calls in try/catch inside onWillStart if you want the component itself to handle the error and show an error state.

Can I re-trigger onWillStart? +

No — onWillStart fires exactly once per component instance. To re-fetch data after mounting (e.g., a "Refresh" button), call your fetch method directly from an event handler. For data that changes when props change, use onWillUpdateProps. If you need to fully reset a component with fresh initial data, unmount and remount it (change its t-key in the parent — OWL treats a different key as a new component instance).

Should I use onWillStart or onMounted for data fetching? +

Use onWillStart when the template depends on the data for its first meaningful render — fetching in onWillStart means one render with complete data. Use onMounted only if the fetch can be done after the DOM is shown (e.g., lazy loading below-the-fold content) and you are comfortable showing a loading placeholder. For most data-driven components, onWillStart gives a cleaner user experience.