0
# Reactivity System
1
2
Fine-grained reactivity system for creating reactive state with automatic UI updates and change tracking.
3
4
## Capabilities
5
6
### reactive
7
8
Creates a reactive proxy object that automatically tracks changes and triggers component re-renders.
9
10
```typescript { .api }
11
/**
12
* Creates a reactive proxy of the target object
13
* @template T - Target object type
14
* @param target - Object to make reactive
15
* @returns Reactive proxy that triggers updates when modified
16
*/
17
function reactive<T extends object>(target: T): T;
18
```
19
20
**Usage Examples:**
21
22
```typescript
23
import { Component, xml, reactive, onMounted } from "@odoo/owl";
24
25
class ReactiveStore extends Component {
26
static template = xml`
27
<div>
28
<h2>User: <t t-esc="store.user.name" /></h2>
29
<p>Posts: <t t-esc="store.posts.length" /></p>
30
<button t-on-click="addPost">Add Post</button>
31
<button t-on-click="updateUser">Update User</button>
32
</div>
33
`;
34
35
setup() {
36
// Create reactive store
37
this.store = reactive({
38
user: {
39
id: 1,
40
name: "John Doe",
41
email: "john@example.com"
42
},
43
posts: [],
44
settings: {
45
theme: "light",
46
notifications: true
47
}
48
});
49
50
onMounted(() => {
51
// Any modification to this.store will trigger re-renders
52
console.log("Store created:", this.store);
53
});
54
}
55
56
addPost() {
57
// Modifying reactive object triggers update
58
this.store.posts.push({
59
id: Date.now(),
60
title: `Post ${this.store.posts.length + 1}`,
61
content: "Lorem ipsum..."
62
});
63
}
64
65
updateUser() {
66
// Nested property updates also trigger reactivity
67
this.store.user.name = "Jane Smith";
68
this.store.user.email = "jane@example.com";
69
}
70
}
71
72
// Global reactive store pattern
73
const globalStore = reactive({
74
currentUser: null,
75
notifications: [],
76
isLoading: false
77
});
78
79
class AppComponent extends Component {
80
static template = xml`
81
<div>
82
<div t-if="globalStore.isLoading">Loading...</div>
83
<div t-else="">
84
<p>User: <t t-esc="globalStore.currentUser?.name || 'Not logged in'" /></p>
85
<p>Notifications: <t t-esc="globalStore.notifications.length" /></p>
86
</div>
87
</div>
88
`;
89
90
setup() {
91
this.globalStore = globalStore;
92
93
// Simulate loading user
94
globalStore.isLoading = true;
95
setTimeout(() => {
96
globalStore.currentUser = { name: "Alice" };
97
globalStore.isLoading = false;
98
}, 1000);
99
}
100
}
101
```
102
103
### markRaw
104
105
Marks an object as non-reactive, preventing it from being converted to a reactive proxy.
106
107
```typescript { .api }
108
/**
109
* Marks an object as non-reactive
110
* @template T - Object type
111
* @param target - Object to mark as raw (non-reactive)
112
* @returns The same object, marked as non-reactive
113
*/
114
function markRaw<T extends object>(target: T): T;
115
```
116
117
**Usage Examples:**
118
119
```typescript
120
import { Component, xml, reactive, markRaw } from "@odoo/owl";
121
122
class DataProcessor extends Component {
123
static template = xml`
124
<div>
125
<p>Processed items: <t t-esc="state.processedCount" /></p>
126
<button t-on-click="processData">Process Data</button>
127
</div>
128
`;
129
130
setup() {
131
// Some objects should not be reactive for performance reasons
132
this.heavyComputationCache = markRaw(new Map());
133
this.domParser = markRaw(new DOMParser());
134
this.workerInstance = markRaw(new Worker("/worker.js"));
135
136
this.state = reactive({
137
processedCount: 0,
138
results: []
139
});
140
}
141
142
processData() {
143
const data = [1, 2, 3, 4, 5];
144
145
data.forEach(item => {
146
// Use non-reactive objects for heavy computations
147
const cacheKey = `item-${item}`;
148
let result = this.heavyComputationCache.get(cacheKey);
149
150
if (!result) {
151
result = this.expensiveComputation(item);
152
this.heavyComputationCache.set(cacheKey, result);
153
}
154
155
// Update reactive state
156
this.state.results.push(result);
157
this.state.processedCount++;
158
});
159
}
160
161
expensiveComputation(item) {
162
// Simulate expensive computation
163
let result = 0;
164
for (let i = 0; i < 1000000; i++) {
165
result += item * Math.random();
166
}
167
return result;
168
}
169
}
170
171
// Third-party library integration
172
class ChartComponent extends Component {
173
static template = xml`
174
<canvas t-ref="canvas" width="400" height="300"></canvas>
175
`;
176
177
setup() {
178
this.canvasRef = useRef("canvas");
179
180
onMounted(() => {
181
// Third-party chart library instance should not be reactive
182
this.chartInstance = markRaw(new ThirdPartyChart(this.canvasRef.el));
183
184
// But chart data can be reactive
185
this.chartData = reactive({
186
labels: ["Jan", "Feb", "Mar"],
187
datasets: [{
188
data: [10, 20, 30],
189
backgroundColor: "blue"
190
}]
191
});
192
193
// Update chart when data changes
194
this.chartInstance.render(this.chartData);
195
});
196
}
197
198
updateData(newData) {
199
// Updating reactive data will trigger chart updates
200
this.chartData.datasets[0].data = newData;
201
this.chartInstance.update(this.chartData);
202
}
203
}
204
```
205
206
### toRaw
207
208
Gets the original (non-reactive) object from a reactive proxy.
209
210
```typescript { .api }
211
/**
212
* Gets the original object from a reactive proxy
213
* @template T - Object type
214
* @param observed - Reactive proxy object
215
* @returns Original non-reactive object
216
*/
217
function toRaw<T>(observed: T): T;
218
```
219
220
**Usage Examples:**
221
222
```typescript
223
import { Component, xml, reactive, toRaw, onMounted } from "@odoo/owl";
224
225
class DataExporter extends Component {
226
static template = xml`
227
<div>
228
<p>Items: <t t-esc="state.items.length" /></p>
229
<button t-on-click="addItem">Add Item</button>
230
<button t-on-click="exportData">Export Data</button>
231
<button t-on-click="compareData">Compare Original vs Reactive</button>
232
</div>
233
`;
234
235
setup() {
236
// Original data
237
this.originalData = {
238
items: [
239
{ id: 1, name: "Item 1" },
240
{ id: 2, name: "Item 2" }
241
],
242
metadata: { version: 1, created: new Date() }
243
};
244
245
// Make it reactive
246
this.state = reactive(this.originalData);
247
}
248
249
addItem() {
250
this.state.items.push({
251
id: Date.now(),
252
name: `Item ${this.state.items.length + 1}`
253
});
254
}
255
256
exportData() {
257
// Get original object for serialization
258
const rawData = toRaw(this.state);
259
260
// Serialize without reactive proxy artifacts
261
const json = JSON.stringify(rawData, null, 2);
262
console.log("Exported data:", json);
263
264
// Create downloadable file
265
const blob = new Blob([json], { type: "application/json" });
266
const url = URL.createObjectURL(blob);
267
const a = document.createElement("a");
268
a.href = url;
269
a.download = "data.json";
270
a.click();
271
URL.revokeObjectURL(url);
272
}
273
274
compareData() {
275
const rawData = toRaw(this.state);
276
277
console.log("Reactive proxy:", this.state);
278
console.log("Original object:", rawData);
279
console.log("Are they the same reference?", rawData === this.originalData);
280
console.log("JSON comparison:", JSON.stringify(rawData) === JSON.stringify(this.originalData));
281
}
282
}
283
284
// Performance-sensitive operations
285
class PerformanceComponent extends Component {
286
static template = xml`
287
<div>
288
<button t-on-click="heavyComputation">Run Heavy Computation</button>
289
<p t-if="state.result">Result: <t t-esc="state.result" /></p>
290
</div>
291
`;
292
293
setup() {
294
this.state = reactive({
295
largeArray: new Array(10000).fill(0).map((_, i) => ({ id: i, value: Math.random() })),
296
result: null
297
});
298
}
299
300
heavyComputation() {
301
// For performance-critical operations, use raw data to avoid proxy overhead
302
const rawArray = toRaw(this.state.largeArray);
303
304
let sum = 0;
305
const start = performance.now();
306
307
// Direct array access without reactive proxy overhead
308
for (let i = 0; i < rawArray.length; i++) {
309
sum += rawArray[i].value;
310
}
311
312
const end = performance.now();
313
314
// Update reactive state with result
315
this.state.result = {
316
sum: sum.toFixed(2),
317
timeMs: (end - start).toFixed(2)
318
};
319
}
320
}
321
322
// Deep cloning utilities
323
class CloneComponent extends Component {
324
static template = xml`
325
<div>
326
<button t-on-click="cloneData">Clone Data</button>
327
<p>Original items: <t t-esc="state.items.length" /></p>
328
<p>Cloned items: <t t-esc="clonedState?.items.length || 0" /></p>
329
</div>
330
`;
331
332
setup() {
333
this.state = reactive({
334
items: [{ id: 1, name: "Original" }],
335
config: { theme: "dark" }
336
});
337
}
338
339
cloneData() {
340
// Get raw data for cloning
341
const rawData = toRaw(this.state);
342
343
// Deep clone the raw data
344
const cloned = JSON.parse(JSON.stringify(rawData));
345
346
// Make the clone reactive
347
this.clonedState = reactive(cloned);
348
349
// Modify clone without affecting original
350
this.clonedState.items.push({ id: 2, name: "Cloned" });
351
352
console.log("Original items:", this.state.items.length);
353
console.log("Cloned items:", this.clonedState.items.length);
354
}
355
}
356
```
357
358
## Reactivity Patterns
359
360
### Store Pattern
361
362
```typescript
363
// Create a global reactive store
364
export const appStore = reactive({
365
user: null,
366
settings: {},
367
notifications: []
368
});
369
370
// Use in components
371
class MyComponent extends Component {
372
setup() {
373
this.store = appStore; // Reference store in template
374
}
375
}
376
```
377
378
### Computed Values Pattern
379
380
```typescript
381
class ComputedComponent extends Component {
382
setup() {
383
this.state = reactive({
384
items: [],
385
filter: "all"
386
});
387
388
// Computed values update automatically when dependencies change
389
Object.defineProperty(this, "filteredItems", {
390
get() {
391
return this.state.items.filter(item =>
392
this.state.filter === "all" || item.status === this.state.filter
393
);
394
}
395
});
396
}
397
}
398
```
399
400
### Performance Considerations
401
402
- Use `markRaw` for large objects that don't need reactivity
403
- Use `toRaw` for performance-critical operations
404
- Avoid creating reactive objects in render loops
405
- Consider breaking large reactive objects into smaller pieces