LWC Reactivity Using Membranes


LWC allows you to create reactive components that automatically update their UI when they get new data, or when existing data is modified. At the heart of this reactive capability is the implementation of a pattern called "membrane". Let's dive into that pattern and how it powers LWC.

What is a membrane

Basically a membrane is a thin layer between the consumer of an object and that object. In other words, a membrane exposes consumer facing proxies for any existing object. From a consumer standpoint, accessing the object through the proxy is indistinguisable from intereacting directly with the object. But the membrane can execute business logic ("hooks") whenever the consumer interacts with the object. It can track property access (read/write), method calls, properties enumeration and so on... It can also hide some content to the consumer, or even distort existing content (e.g. change the values being returned). It is a very powerful pattern used for many different purposes, from content isolation to change tracking.

Another important point with a membrane is that any object accessed through a proxy is automatically proxied by the membrane as well. Let's note a proxied object X as P(X).
If A has a property b of type B, then A.b returns a object of type B. But P(A).b returns P(B). The membrane hooked the access to the property b and distorted the result to return a proxy object for b. This is obviously transparent to the consumer:
Because of this, if one starts from a proxied root object and only access the graph from this root object, then it guarantees that all the objects accessed are proxies, and are thus managed by the membrane.

Finally, membranes have another important property: there is a one to one relationship between an object and its proxy for a given membrane. It means that the same P(A) object is returned for a given object A, regardless how it is retrieved. Suppose, for example, that a component has list of child components and a reference to its parent. The equality bellow is true:
         comp.children[0].parent === comp
That equality is preserved with proxies as well:
         P(comp).children[0].parent === P(comp)
More detailed information on membranes can be found here: membrane.

Salesforce Membrane Implementation

As said earlier, LWC is using membranes to implement reactivity. But, instead of coding membranes within LWC, Salesforce created a separate general purpose, open source library called observable-membrane. Like other JavaScript implementations, it is based on top of the Proxy API.

With this library, a developer can create a membrane and proxy objects with the simple code bellow:
  const membrane = new ObservableMembrane();
  ...
  const o = membrane.getProxy(o);

The membrane object holds a set of functions called when a caller interacts with a proxied object. Thus, the membrane can track when a value is read/updated and eventually distort that value. In short, the ObservableMembrane knows about any property access/change in the graph starting from any proxied object, obtained by calling getProxy().

LWC and Membrane

This is where it becomes interesting: if a membrane can track any change in an object graph, it means that it can re-render a component when it detects a change in the data graph it consumes. This is exactly what the @track property decorator is for:

  class Banner extends LightningElement {
      @track user
  }

This decorator ensures that any value assigned to the decorated property is actually proxied using the reactiveMembrane. Typically, assigning a value to a tracked property like:
  this.user={ name: 'Paul', ...}
is internally equivalent to:
  this.user=reactiveMembrane.getProxy({name: 'Paul', ...}).

reactiveMembrane is a singleton, defined in the LWC engine library. It tracks the value changes in any object it created proxies for, find the components impacted by the changes and re-render them when necessary.  For example, if a component has a onclick event handler like bellow:
  handleClick() {
    this.user.name = 'Bob';
  } 
The user name change is detected by the reactiveMembrane and the component is re-rendered without any further work from the developer.

Note that the re-rendering does not happen synchronously when the change is detected. The component is internally tagged as 'dirty' and the rendering will happen later. Thus, sequentially changing multiple tracked values for a component only triggers a single re-rendering.

Multiple components can track changes of the same object. Fortunately, every component will be updated when a change happens to this object, regardless where and how it was updated.

Finally, all the fields in a component are reactive. Adding @track to a component property is only needed for values you'd like to track deeper, like objects or array, not for basic strings, numbers or booleans.

Conclusion

The use of membranes within LWC makes it very easy to track changes and react when they happen. The developers don't have to care about notifying the system when some data is changed, it is "magic". From an implementation standpoint, the use of the Proxy API also makes it very efficient at runtime.

But wait: it reveals all its power when coupled with other LWC technologies. Let's explore that in the next post!

Comments