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.
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;
});
}
}
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
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
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>
`;
}
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.
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;
}
}
}
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
/** @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
| Hook | Fires when | Async? | 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
onWillStartfires 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
asyncfunction or return a Promise and OWL will wait. - Inside
onWillUpdateProps, comparenextProps.xtothis.props.xto 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
onWillStartsets 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:
- Props:
query(String) — the search term. - In
onWillStart, simulate an API call withawait delay(500)(a helper that returnsnew Promise(r => setTimeout(r, 500))). Set fake results based on the query. - In
onWillUpdateProps, check ifnextProps.query !== this.props.query. If so, setstate.isLoading = true, simulate the fetch, and update results. - Show a loading indicator while loading. Show "No results" when the results array is empty.
- In a parent
App, add an input bound tostate.queryviat-model. Passstate.querytoSearchResults. Observe: every time you type and the query changes,SearchResultsshows a loading state then updates.
Frequently Asked Questions
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).
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.
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).
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.