Enable Dark Mode!
an-overview-of-owl-reactivity-system-in-odoo-16.jpg
By: Renu M

An Overview of Owl Reactivity System in Odoo 16

Technical Odoo 16

Reactivity is a prominent subject in JavaScript frameworks, aiming to simplify state manipulation while ensuring automatic interface updates based on state changes. In the context of Owl, a JavaScript framework, a proxy-based reactivity system is employed, utilizing the "reactive" primitive. A proxy object is returned by utilizing the "reactive" function, which takes an object as the first argument and an optional callback as the second argument. This proxy object keeps track of property accesses made through it and triggers the provided callback whenever any of these properties are modified through any reactive instance of the same object. It also enables the retrieval of reactive versions of subobjects when accessed.

useState

Although the reactive primitive is powerful, its usage in components often follows a standard pattern. Components need to be re-rendered when the portion of the state they rely on for rendering undergoes changes. To facilitate this, Owl offers a standard hook called useState. Essentially, this hook invokes the reactive function with the provided object and the current component's render function as the callback. As a result, the component will re-render whenever any part of the state object, accessed by the component, is modified.
Here's a straightforward example demonstrating the usage of useState:

class Counter extends Component {
  static template = xml`
    <div t-on-click="() => this.state.value++">
      <t t-esc="state.value"/>
    </div>`;
  setup() {
    this.state = useState({ value: 0 });
  }
}

In this component, the state. value is read during rendering, establishing a subscription to changes made to that particular key. Whenever the value changes, Owl updates the component accordingly. It's worth noting that the state property is not special; you can choose any name for your state variables and have multiple state variables within the same component if it's appropriate. This flexibility also allows the useState hook to be utilized in custom hooks that may require state-specific to those hooks.

Reactive Props

Starting from version 2.0, Owl renders are no longer "deep" by default, meaning a component is only re-rendered by its parent if its props have changed based on a simple equality test. However, what if the contents of a prop have changed within a deeper property? In such cases, if that prop is reactive, Owl will automatically re-render the child components that need to be updated, and only those components. This is achieved by re-observing reactive objects passed as props to components.

Consider the following example:

class Counter extends Component {
  static template = xml`
    <div t-on-click="() => props.state.value++">
      <t t-esc="props.state.value"/>
    </div>`;
}
class Parent extends Component {
  static template = xml`
    <Counter state="this.state"/>
    <button t-on-click="() => this.state.value = 0">Reset counter</button>
    <button t-on-click="() => this.state.test++" t-esc="this.state.test"/>`;
  setup() {
    this.state = useState({ value: 0, test: 1 });
  }
}

In this example, when the counter button is clicked, only the Counter component is re-rendered because the Parent component has never accessed the "value" key in the state. Similarly, when the "Reset Counter" button is clicked, only the Counter component undergoes re-rendering. The crucial aspect is not where the state is updated, but rather which parts of the state are modified and which components depend on them. Owl accomplishes this by automatically employing the useState hook on reactive objects passed as props to child components.

In the scenario where the last button is clicked, the Parent component is re-rendered, but the child (Counter) component remains unaffected since it doesn't utilize the "test" key. The props assigned to it (this.state) have also remained unchanged. Consequently, the Parent component updates while the child component remains unaltered.

Reactive

The reactive function is a core mechanism for enabling reactivity in Owl. It accepts an object or array as its first argument, and optionally, a callback function as its second argument. Whenever a tracked value within the object or array is updated, the callback function is invoked.

Here's an example of how reactive can be used:

const obj = reactive({ a: 1 }, () => console.log("changed"));

obj.a = 2; // No log is generated because 'a' hasn't been accessed yet

console.log(obj.a); // Logs 2 and marks 'a' as tracked

obj.a = 3; // Logs 'changed' because a tracked value has been updated

Reactive objects have the ability to be reobserved, creating independent proxies that track a separate set of keys:

const obj1 = reactive({ a: 1, b: 2 }, () => console.log("observer 1"));

const obj2 = reactive(obj1, () => console.log("observer 2"));

console.log(obj1.a); // Logs 1 and marks 'a' as tracked by observer 1

console.log(obj2.b); // Logs 2 and marks 'b' as tracked by observer 2

obj2.a = 3; // Only logs 'observer1' because observer 2 doesn't track 'a'

obj2.b = 3; // Only logs 'observer2' because observer 1 doesn't track 'b'

console.log(obj2.a, obj1.b); // Logs 3 and 3; though observed independently, it remains a single object

When using useState, which returns a regular reactive object, you can call reactive on the result to observe changes to that object outside of the component context. Similarly, you can also apply useState on reactive objects created outside of components. However, be cautious about the lifespan of these reactive objects, as retaining references to them may prevent the component and its data from being garbage-collected, even after it has been destroyed by Owl.

Subscriptions are ephemeral

Subscriptions to state changes are temporary. When an observer is notified of a change in a state object, all its subscriptions are immediately cleared. This means that if the observer still needs to track changes, it must re-read the relevant properties. Let's consider an example to illustrate this behaviour:

const obj = reactive({ a: 1 }, () => console.log("observer called"));

console.log(obj.a); // Outputs 1 and marks 'a' as tracked by the observer

obj.a = 3; // Triggers 'observer called' and clears the observer's subscriptions

obj.a = 4; // No output is generated because the key is no longer observed

While this behaviour may initially seem counterintuitive, it aligns well with the behaviour of components. Let's examine a component scenario:

class DoubleCounter extends Component {
  static template = xml`
    <t t-esc="state.selected + ': ' + state[state.selected].value"/>
    <button t-on-click="() => this.state.count1++">increment count 1</button>
    <button t-on-click="() => this.state.count2++">increment count 2</button>
    <button t-on-click="changeCounter">Switch counter</button>`;
  setup() {
    this.state = useState({ selected: "count1", count1: 0, count2: 0 });
  }
  changeCounter() {
    this.state.selected = this.state.selected === "count1" ? "count2" : "count1";
  }
}

In this component, if we increment the value of the second counter, the component will not be re-rendered. This behavior is logical since re-rendering would have no effect as the second counter is not displayed. When we toggle the component to display the second counter, we no longer want the component to re-render when the value of the first counter changes. And that's precisely what happens: a component only re-renders when there are changes to state properties that have been accessed during or after the previous render. If a state property hasn't been accessed during the last render, we can infer that its value won't impact the rendered output, and therefore it can be disregarded.

Escape Hatches

In certain scenarios, it is useful to bypass the reactivity system to avoid the overhead of creating proxies when interacting with reactive objects. Although the performance gains from selectively rerendering relevant parts of the interface generally outweigh this cost, there are situations where it is desirable to opt out of proxy creation entirely. This is precisely the purpose of the markRaw function.

markRaw

The markRaw function allows an object to be excluded from the reactivity system. If an object marked with markRaw is part of a reactive object, it will be returned as is without creating proxies, and none of its keys will be observed.

For example:

const someObject = markRaw({ b: 1 });
const state = useState({
  a: 1,
  obj: someObject,
});
console.log(state.obj.b); // Attempt to subscribe to the "b" key in someObject
state.obj.b = 2; // No rerender will occur here
console.log(someObject === state.obj); // true

This can be useful in certain cases. One example is when you have a large array of immutable objects that you want to render in a list. By marking these objects as raw, you avoid the creation of unnecessary reactive objects while still being able to track their presence and identity using reactivity.

However, it's important to use markRaw with caution. It is an escape hatch from the reactivity system and can lead to unintended issues. For instance, modifying a marked raw object will not trigger a rerender, potentially causing the UI to become desynchronized from the component's state until the next render caused by another action.

In contrast, the toRaw function takes a reactive object and returns the underlying non-reactive object. This can be useful in niche cases, such as when you need to compare the original object with a reactive proxy or for debugging purposes when working with proxies can be confusing.

const obj = {};

const reactiveObj = reactive(obj);

console.log(obj === reactiveObj); // false

console.log(obj === toRaw(reactiveObj)); // true

Advanced Usage:

Advanced usage of the reactivity system in Owl allows developers to leverage its power for solving various challenges in JavaScript applications. Here are a few examples:

Notification Manager: Using Owl's reactivity system, you can create a notification manager that automatically updates the UI when new notifications are added or removed. By tracking changes to a reactive array of notifications, you can display real-time notifications to the user.

Store: The reactivity system in Owl can be utilized to implement a centralized store for application state management. By defining a reactive object as the store, components can easily subscribe to the relevant parts of the state and automatically update when changes occur, enabling efficient state management across the application.

Local Storage Synchronization: With Owl's reactivity system, you can create a custom hook to synchronize the application state with the browser's local storage. By making the reactive state persist between page reloads, you can provide a seamless user experience while preserving data integrity.

These examples highlight the flexibility and versatility of Owl's reactivity system, allowing developers to create dynamic and responsive JavaScript applications with ease. By harnessing the power of reactivity, complex tasks such as managing notifications, state, and data synchronization can be simplified and automated, enhancing the overall user experience.


If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



whatsapp
location

Calicut

Cybrosys Technologies Pvt. Ltd.
Neospace, Kinfra Techno Park
Kakkancherry, Calicut
Kerala, India - 673635

location

Kochi

Cybrosys Technologies Pvt. Ltd.
1st Floor, Thapasya Building,
Infopark, Kakkanad,
Kochi, India - 682030.

location

Bangalore

Cybrosys Techno Solutions
The Estate, 8th Floor,
Dickenson Road,
Bangalore, India - 560042

Send Us A Message