or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

app-components.mdblockdom.mdhooks.mdindex.mdlifecycle.mdreactivity.mdtemplates.mdutils-validation.md

reactivity.mddocs/

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