useEnv() — The Shared Environment
Every OWL component has access to a shared env object. useEnv() returns it. In a standalone app you populate it when calling mount(); in Odoo it is already populated by the framework.
import { mount } from "@odoo/owl";
import App from "./App";
// Pass env via mount options — available to ALL descendants
mount(App, document.getElementById("app"), {
env: {
// Any data or functions you want globally available
config: {
apiBase: "https://api.example.com",
theme: "light",
locale: "en-US",
},
formatDate(date) {
return new Intl.DateTimeFormat("en-US").format(date);
},
},
});
import { Component, xml, useEnv } from "@odoo/owl";
class InvoiceDate extends Component {
static template = xml`
<span t-esc="formatted"/>
`;
setup() {
// Access the shared environment — works at ANY depth in the tree
this.env = useEnv();
}
get formatted() {
// Use the shared formatDate function from env
return this.env.formatDate(this.props.date);
}
}
useService() in Odoo
Odoo extends the OWL environment with a services registry. useService("serviceName") is a custom hook that reads the named service from env.services. Services are singletons — one instance per application.
/** @odoo-module **/
import { Component, xml, useState, onWillStart } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
class SaleOrderList extends Component {
static template = xml`
<div>
<t t-if="state.isLoading"><span>Loading…</span></t>
<table t-else="">
<tr t-foreach="state.orders" t-as="order" t-key="order.id">
<td t-esc="order.name"/>
<td t-esc="order.partner_id[1]"/>
<td t-esc="order.amount_total"/>
<td>
<button t-on-click="() => openOrder(order.id)">Open</button>
</td>
</tr>
</table>
</div>
`;
state = useState({ orders: [], isLoading: true });
setup() {
// Inject services — call in setup() only
this.orm = useService("orm");
this.action = useService("action");
this.notification = useService("notification");
this.user = useService("user");
onWillStart(() => this.loadOrders());
}
async loadOrders() {
try {
this.state.orders = await this.orm.searchRead(
"sale.order",
[["user_id", "=", this.user.userId]],
["name", "partner_id", "amount_total", "state"],
{ limit: 50, order: "date_order desc" }
);
} catch {
this.notification.add("Failed to load orders", { type: "danger" });
} finally {
this.state.isLoading = false;
}
}
openOrder(id) {
this.action.doAction({
type: "ir.actions.act_window",
res_model: "sale.order",
res_id: id,
views: [[false, "form"]],
});
}
}
Odoo Services Reference
| Service name | Import | Key methods / properties |
|---|---|---|
"orm" | useService("orm") | read(model, ids, fields), searchRead(model, domain, fields), write(model, ids, vals), create(model, vals), unlink(model, ids), call(model, method, args) |
"notification" | useService("notification") | add(message, { type, sticky, buttons }) — type: "info", "success", "warning", "danger" |
"action" | useService("action") | doAction(action), doAction(xmlid), switchView(type) |
"user" | useService("user") | userId, name, lang, tz, isAdmin, hasGroup(xmlid) |
"router" | useService("router") | navigate(href), current.hash, pushState(state) |
"dialog" | useService("dialog") | add(DialogComponent, props) — open dialogs programmatically |
"company" | useService("company") | currentCompany, allowedCompanies, currency info |
"rpc" | useService("rpc") | Low-level RPC calls (prefer orm service when possible) |
env vs Props — When to Use Each
| Use props when | Use env/services when |
|---|---|
| Data is specific to one component or its subtree | Data/capability needed by many components at any depth |
| Parent explicitly controls what child receives | Global cross-cutting concerns (auth, theming, i18n, ORM) |
| The relationship is direct parent → child | 3+ levels of depth (prop drilling becomes unmanageable) |
| You want type-safe, documented component interface | Singletons that must not be duplicated |
The env: Shared Context for the Whole Component Tree
The env is a single object shared by every component in the tree. It's how app-wide things — services, configuration, translation functions — reach any component without being threaded through props at every level ("prop drilling").
import { useEnv, useService } from "@web/core/utils/hooks";
class Widget extends Component {
setup() {
this.env; // the shared environment object
this.orm = useService("orm"); // services live on the env
// this.env.bus, this.env._t (translation), etc.
}
}
Services (ORM, notification, dialog) are registered onto the env at startup, which is exactly why useService("orm") works in any component — it's reading from the shared env under the hood.
| Access | Use for |
|---|---|
useEnv() | read the env directly |
useService(name) | grab a service (the common case) |
| sub-environment | override env for a subtree |
Why it exists: passing a notification service down through ten layers of props would be miserable and fragile. The env gives descendants a shared channel for truly global concerns. Sub-environments: a component can extend the env for its subtree (e.g. provide a scoped config or a form context) — children see the additions, the rest of the app doesn't. Guideline: use the env for cross-cutting infrastructure (services, i18n, buses); use ordinary props for the specific data a component needs. Overstuffing the env turns it into a hidden global.
🏋️ Practical Exercise
Build a CurrentUserBadge Odoo component using services:
- Use
useService("user")to get the current user's name, language, and timezone. - Display the name with a greeting based on time of day (Morning/Afternoon/Evening) — compute it from
new Date().getHours(). - Add a "Logout" button that calls
window.location.href = "/web/session/logout". - Add an "Admin Panel" button visible only when
user.isAdminis true. Clicking it callsaction.doAction("base_setup.action_general_configuration"). - Show a notification using
useService("notification")when the component mounts: "Welcome back, [name]!".
🔥 Challenge Exercise
Use the OWL environment (env) to share global services/config down the component tree without prop-drilling — e.g. a translation function or a service. Explain what the env is, how it is provided at mount, and when to use it vs props.
📋 Summary
useEnv()returns the shared environment object available to all components — the OWL equivalent of React Context.- In standalone apps, provide the env via the
envoption ofmount(). - In Odoo, use
useService("name")(from@web/core/utils/hooks) to inject framework services. - Key Odoo services:
orm(database),notification(toasts),action(navigation),user(current user),dialog(programmatic dialogs). - Services are singletons — one instance per app — injected wherever needed without prop drilling.
- Always call
useService()insetup()— not in methods or async callbacks.
Interview Questions
- What is the OWL environment (
env)? - How do you access the env?
- How is the env provided?
- When would you use env instead of props?
- What typically lives in the env?
Related Topics
FAQ
The env object itself is frozen in OWL (you cannot add or delete top-level keys after mount). However, if your env contains reactive objects (created with reactive() from OWL), mutations on those objects will cause components that read them to re-render. For Odoo, env mutation is managed by the framework — you do not modify it directly.
useService is an Odoo-specific utility in @web/core/utils/hooks. It reads from env.services which Odoo populates. In a standalone OWL app (without Odoo), use useEnv() directly and read whatever you put in the env. You can implement your own service-like pattern by putting service objects into the env at mount() time.

