Odoo Web Library (OWL) is a modern, lightweight TypeScript UI framework for building reactive web applications with components, templates, and state management.
Fine-grained reactivity system for creating reactive state with automatic UI updates and change tracking.
Creates a reactive proxy object that automatically tracks changes and triggers component re-renders.
/**
* Creates a reactive proxy of the target object
* @template T - Target object type
* @param target - Object to make reactive
* @returns Reactive proxy that triggers updates when modified
*/
function reactive<T extends object>(target: T): T;Usage Examples:
import { Component, xml, reactive, onMounted } from "@odoo/owl";
class ReactiveStore extends Component {
static template = xml`
<div>
<h2>User: <t t-esc="store.user.name" /></h2>
<p>Posts: <t t-esc="store.posts.length" /></p>
<button t-on-click="addPost">Add Post</button>
<button t-on-click="updateUser">Update User</button>
</div>
`;
setup() {
// Create reactive store
this.store = reactive({
user: {
id: 1,
name: "John Doe",
email: "john@example.com"
},
posts: [],
settings: {
theme: "light",
notifications: true
}
});
onMounted(() => {
// Any modification to this.store will trigger re-renders
console.log("Store created:", this.store);
});
}
addPost() {
// Modifying reactive object triggers update
this.store.posts.push({
id: Date.now(),
title: `Post ${this.store.posts.length + 1}`,
content: "Lorem ipsum..."
});
}
updateUser() {
// Nested property updates also trigger reactivity
this.store.user.name = "Jane Smith";
this.store.user.email = "jane@example.com";
}
}
// Global reactive store pattern
const globalStore = reactive({
currentUser: null,
notifications: [],
isLoading: false
});
class AppComponent extends Component {
static template = xml`
<div>
<div t-if="globalStore.isLoading">Loading...</div>
<div t-else="">
<p>User: <t t-esc="globalStore.currentUser?.name || 'Not logged in'" /></p>
<p>Notifications: <t t-esc="globalStore.notifications.length" /></p>
</div>
</div>
`;
setup() {
this.globalStore = globalStore;
// Simulate loading user
globalStore.isLoading = true;
setTimeout(() => {
globalStore.currentUser = { name: "Alice" };
globalStore.isLoading = false;
}, 1000);
}
}Marks an object as non-reactive, preventing it from being converted to a reactive proxy.
/**
* Marks an object as non-reactive
* @template T - Object type
* @param target - Object to mark as raw (non-reactive)
* @returns The same object, marked as non-reactive
*/
function markRaw<T extends object>(target: T): T;Usage Examples:
import { Component, xml, reactive, markRaw } from "@odoo/owl";
class DataProcessor extends Component {
static template = xml`
<div>
<p>Processed items: <t t-esc="state.processedCount" /></p>
<button t-on-click="processData">Process Data</button>
</div>
`;
setup() {
// Some objects should not be reactive for performance reasons
this.heavyComputationCache = markRaw(new Map());
this.domParser = markRaw(new DOMParser());
this.workerInstance = markRaw(new Worker("/worker.js"));
this.state = reactive({
processedCount: 0,
results: []
});
}
processData() {
const data = [1, 2, 3, 4, 5];
data.forEach(item => {
// Use non-reactive objects for heavy computations
const cacheKey = `item-${item}`;
let result = this.heavyComputationCache.get(cacheKey);
if (!result) {
result = this.expensiveComputation(item);
this.heavyComputationCache.set(cacheKey, result);
}
// Update reactive state
this.state.results.push(result);
this.state.processedCount++;
});
}
expensiveComputation(item) {
// Simulate expensive computation
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += item * Math.random();
}
return result;
}
}
// Third-party library integration
class ChartComponent extends Component {
static template = xml`
<canvas t-ref="canvas" width="400" height="300"></canvas>
`;
setup() {
this.canvasRef = useRef("canvas");
onMounted(() => {
// Third-party chart library instance should not be reactive
this.chartInstance = markRaw(new ThirdPartyChart(this.canvasRef.el));
// But chart data can be reactive
this.chartData = reactive({
labels: ["Jan", "Feb", "Mar"],
datasets: [{
data: [10, 20, 30],
backgroundColor: "blue"
}]
});
// Update chart when data changes
this.chartInstance.render(this.chartData);
});
}
updateData(newData) {
// Updating reactive data will trigger chart updates
this.chartData.datasets[0].data = newData;
this.chartInstance.update(this.chartData);
}
}Gets the original (non-reactive) object from a reactive proxy.
/**
* Gets the original object from a reactive proxy
* @template T - Object type
* @param observed - Reactive proxy object
* @returns Original non-reactive object
*/
function toRaw<T>(observed: T): T;Usage Examples:
import { Component, xml, reactive, toRaw, onMounted } from "@odoo/owl";
class DataExporter extends Component {
static template = xml`
<div>
<p>Items: <t t-esc="state.items.length" /></p>
<button t-on-click="addItem">Add Item</button>
<button t-on-click="exportData">Export Data</button>
<button t-on-click="compareData">Compare Original vs Reactive</button>
</div>
`;
setup() {
// Original data
this.originalData = {
items: [
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" }
],
metadata: { version: 1, created: new Date() }
};
// Make it reactive
this.state = reactive(this.originalData);
}
addItem() {
this.state.items.push({
id: Date.now(),
name: `Item ${this.state.items.length + 1}`
});
}
exportData() {
// Get original object for serialization
const rawData = toRaw(this.state);
// Serialize without reactive proxy artifacts
const json = JSON.stringify(rawData, null, 2);
console.log("Exported data:", json);
// Create downloadable file
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "data.json";
a.click();
URL.revokeObjectURL(url);
}
compareData() {
const rawData = toRaw(this.state);
console.log("Reactive proxy:", this.state);
console.log("Original object:", rawData);
console.log("Are they the same reference?", rawData === this.originalData);
console.log("JSON comparison:", JSON.stringify(rawData) === JSON.stringify(this.originalData));
}
}
// Performance-sensitive operations
class PerformanceComponent extends Component {
static template = xml`
<div>
<button t-on-click="heavyComputation">Run Heavy Computation</button>
<p t-if="state.result">Result: <t t-esc="state.result" /></p>
</div>
`;
setup() {
this.state = reactive({
largeArray: new Array(10000).fill(0).map((_, i) => ({ id: i, value: Math.random() })),
result: null
});
}
heavyComputation() {
// For performance-critical operations, use raw data to avoid proxy overhead
const rawArray = toRaw(this.state.largeArray);
let sum = 0;
const start = performance.now();
// Direct array access without reactive proxy overhead
for (let i = 0; i < rawArray.length; i++) {
sum += rawArray[i].value;
}
const end = performance.now();
// Update reactive state with result
this.state.result = {
sum: sum.toFixed(2),
timeMs: (end - start).toFixed(2)
};
}
}
// Deep cloning utilities
class CloneComponent extends Component {
static template = xml`
<div>
<button t-on-click="cloneData">Clone Data</button>
<p>Original items: <t t-esc="state.items.length" /></p>
<p>Cloned items: <t t-esc="clonedState?.items.length || 0" /></p>
</div>
`;
setup() {
this.state = reactive({
items: [{ id: 1, name: "Original" }],
config: { theme: "dark" }
});
}
cloneData() {
// Get raw data for cloning
const rawData = toRaw(this.state);
// Deep clone the raw data
const cloned = JSON.parse(JSON.stringify(rawData));
// Make the clone reactive
this.clonedState = reactive(cloned);
// Modify clone without affecting original
this.clonedState.items.push({ id: 2, name: "Cloned" });
console.log("Original items:", this.state.items.length);
console.log("Cloned items:", this.clonedState.items.length);
}
}// Create a global reactive store
export const appStore = reactive({
user: null,
settings: {},
notifications: []
});
// Use in components
class MyComponent extends Component {
setup() {
this.store = appStore; // Reference store in template
}
}class ComputedComponent extends Component {
setup() {
this.state = reactive({
items: [],
filter: "all"
});
// Computed values update automatically when dependencies change
Object.defineProperty(this, "filteredItems", {
get() {
return this.state.items.filter(item =>
this.state.filter === "all" || item.status === this.state.filter
);
}
});
}
}markRaw for large objects that don't need reactivitytoRaw for performance-critical operationsInstall with Tessl CLI
npx tessl i tessl/npm-odoo--owl