CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-odoo--owl

Odoo Web Library (OWL) is a modern, lightweight TypeScript UI framework for building reactive web applications with components, templates, and state management.

Overview
Eval results
Files

reactivity.mddocs/

Reactivity System

Fine-grained reactivity system for creating reactive state with automatic UI updates and change tracking.

Capabilities

reactive

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);
  }
}

markRaw

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);
  }
}

toRaw

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);
  }
}

Reactivity Patterns

Store Pattern

// 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
  }
}

Computed Values Pattern

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
        );
      }
    });
  }
}

Performance Considerations

  • Use markRaw for large objects that don't need reactivity
  • Use toRaw for performance-critical operations
  • Avoid creating reactive objects in render loops
  • Consider breaking large reactive objects into smaller pieces

Install with Tessl CLI

npx tessl i tessl/npm-odoo--owl

docs

app-components.md

blockdom.md

hooks.md

index.md

lifecycle.md

reactivity.md

templates.md

utils-validation.md

tile.json