LWC Wire Adapters #2 - Producing Data Streams


We saw in the previous post that wire adapters are a very elegant and efficient way to consume data streams. Let's see now how we can produce these data streams. If it seems magic, it is actually a pretty easy API.

*Disclaimers*
#1 - This post talks about the original wire adapter API, as described here.  There is an upcoming new implementation that keeps the same consumer interface, but where the provider API is a bit different, see wire-reform. This blog post talks about the former, and will be updated when the later will be released.
#2 -  Custom wire adapters are currently only available to the Open Source version of LWC, not to LWC running in Salesforce Core.

Our First Wire Adapter

To introduce the wire adapter API, let's implement a stopWatch component that behind the scene uses a wire adapter streaming the time elapsed. This wire adapter offers some callbacks to start, stop and reset the timer:

For the impatient, the source code is provided through the LWC playground:

Wire Adapters Registry

LWC maintains a global registry of wire adapters, so each wire adapter has to be registered before it can be consumed by a component. It is uniquely identified by a key, which can be a Symbol or a Function. As a good practice, this key is exported from the wire adapter JavaScript module. The registration is done through the register method:

  register(key, myAdapterFunction);


The current implementation also requires a global, LWC provided, "wire service" to be registered before any wire adapter can be used. The main file of an application is generally a good place for it.

  import { register } from "lwc";
  // The wire service has to be registered once
  import { registerWireService } from 'wire-service';
  registerWireService(register);

Note that the example above does not have to do this, as the Playground does it automatically for any code snippet.

Wire Adapter Implementation

A wire adapter is defined by a function that is called once when the wire adapter is initialized. It has a unique parameter, eventTarget, which represents the consumer of this wire adapter.

To get started, here is how our simple wire adapter skeleton stopWatch looks like:

  // Define the Symbol identifying the wire adapter
  export const stopWatch = Symbol('stop-watch');
 
  // Register the wire adapter implementation
  register(
stopWatch, eventTarget => {
      // wire adapter implementation
      // ...
  });
  • stopWatch
    This is the wire adapter key, exported so components can reference it.
  • eventTarget
    The context passed by the engine, see bellow.

Initialization

The wire adapter API is event based: it receives notifications when it has to take an action. In fact it can register handlers for the following 3 different events:
  • connect
    This event is emitted when the component owning the wire adapter is connected to the DOM. At that time, it is safe for the wire adapter to send data to the component.
  • disconnect
    This event is emitted when the component owning the wire adapter is disconnected from the DOM. When the component is disconnected, the wire adapter can cleanup its internal data. For example, if it was observing a data stream, it can disconnect from it. Once a component is disconnected, the wire adapter should no longer send it data, until it connects again.
  • config
    This is emitted once initially, and thereafter when the wire adapter options have different values ("reactive options"). The options value are passed as an argument to the handler.
To handle these events, the wire adapter has to register handlers to the eventTarget:

  register(stopWatch, eventTarget => {

    function handleConfig(options) {
      // Do something with the config
    }

    function handleConnect() {
      
      // The component is connected, send it some values! 
    }

    function handleDisconnect() {
      // The component is disconnected, cleanup the adapter
    }

    // Connect the wire adapter
    eventTarget.addEventListener('config', handleConfig);
    eventTarget.addEventListener('connect', handleConnect);
    eventTarget.addEventListener('disconnect', handleDisconnect);
  })

Sending data to the component

Once the wire adapter receives the connect event, it can send data to the component. For this, it dispatches a ValueChangedEvent to the component with the new value as an argument:

  const result = ...
  eventTarget.dispatchEvent(new ValueChangedEvent(result));


Once connected, the wire adapter can send new data to the component at anytime. For example, if the component is listening to a websocket, it can update the component when its gets new data from that websocket.

Assembling the pieces 

Well, it is assembled in the playground sample code:
  • stopwatch.js contains the wire adapter code
  • app.js/.html defines the component.

Note: there are 2 differences between this code running within the playground and a potential standalone application:
  1. The wire service does not have to be registered, the playground does it
  2. The wire adapter in the playground imports wire-service, while it should be @lwc/wire-service in an app using LWC Open Source.

A few implementation details

The wire adapter uses a configuration option to set the refresh interval. In the example, the value is passed through a reactive property. This makes the handleConfig() handler called with a new value when this property changes:
  @track interval = 50;
  @wire(timeWatch,{interval:'$interval'}) stopWatch;


The data sent by the wire adapter to the component contains both the timer values (hour, mins, ...) as well as some callback functions to interact with the stopwatch adapter. Here is a mock of that object:
 {
    // Timer values
    hour: '00',
    mins: '00',
    secs: '00',
    mils: '000',
    // Callback functions
    start: function() {...},
    stop
: function() {...},
    reset: function() {...},  
}

To make the wire adapter robust, it maintains a connected flag and only sends data to the component when it this flag is true.

This wire adapter implementation creates and sends a copy of the date to the component.  Sending the same object instance multiple times to the component does not make the component to re-render, while a copy does.

Finally, wire adapters are simple to implement, aren't they?

Comments