Post

Queueing Wire Calls in Lightning Web Components

Queueing Wire Calls in Lightning Web Components

What is wire call?

In salesforce Lightning Web Components (Hereafter would be referred as LWC), we have two ways to get data from the apex (i.e. backend).

  • Javascript API
  • Wire Adapter

Unlike Javascript API,

  • wire adapter gives control to the LWC Engine and will fetch new data by itself when the variable passed to it changes.
  • wire adapter only allows in data read.
1
2
3
4
import { wire } from "lwc";
export default class WireClass extends LightningElement {
  @wire(adapterName, adapterConfig) adapterResult;
}

Difference between wire property and wire methods

Wire adapter can be tagged to a property or method.

If it’s tagged to property as below, the property will have data and error key, which we can access in other methods.

1
2
3
4
5
@wire(adapterName, adapterConfig) wireProperty;

someMethod() {
  console.log(this.wireProperty.data);
}

If it’s tagged to method as below, the method will have one argument, which is an object containing data and error as keys.

1
2
3
4
5
6
7
@wire(adapterName, adapterConfig) wireMethod({data, error}) {
  if(data) {
    console.log(data);
  } else if (error) {
    console.error(error);
  }
}

Thus wire method provide better data handling than wire property.

What is the issue with wire adapter?

Wire methods are asynchronous, i.e. if we have multiple wire adapters in an LWC, we won’t be sure which one would be completed first. Some times the wire call finishes before the connectedcallback itself. That’s the power of LWC engine and it’s very helpful when the wire calls are not dependent on one another.

Here is one such example where wire adapters fails! Hope you know getPicklistValue from objectInfoAPI and getRecord from uiRecordAPI. These are salesforce methods to get the data from the salesforce org to the LWC. In this scenario, we have two picklist SC_Parent_Picklist__c and SC_Child_Picklist__c under an object named SC_Queue_Wire_Call__c where SC_Child_Picklist__c is dependent on SC_Parent_Picklist__c. And we need to have this picklist values inside a lightning-combobox, and that needs to be prepopulated based on the recordId.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import { wire, api } from "lwc";
import { getRecord } from "lightning/uiRecordApi";
import { getPicklistValues } from "lightning/uiObjectInfoApi";
import PARENT_PICKLIST_FIELD from "@salesforce/schema/SC_Queue_Wire_Call__c.SC_Parent_Picklist__c";
import CHILD_PICKLIST_FIELD from "@salesforce/schema/SC_Queue_Wire_Call__c.SC_Child_Picklist__c";

export default class WireClass extends LightningElement {
  @api recordId;

  parentValueToIndexMap;
  parentPicklistValue;
  childPicklistValue;
  childPicklistValue;
  parentPicklistOptions = [];
  childPicklistOptions = [];
  allChildPicklistOptions = [];

  @wire(getPicklistValues, {
    recordTypeId: "012000000000000AAA", // default recordtypeid
    fieldApiName: PARENT_PICKLIST_FIELD,
  })
  wiredGetParentPicklistValues({ data, error }) {
    if (data) {
      this.parentPicklistOptions = data;
      this.parentValueToIndexMap = data.reduce((acc, option, index) => {
        acc[option.value] = index;
        return acc;
      }, {});
    }
  }

  @wire(getPicklistValues, {
    recordTypeId: "012000000000000AAA", // default recordtypeid
    fieldApiName: CHILD_PICKLIST_FIELD,
  })
  wiredGetChildPicklistValues({ data, error }) {
    if (data) {
      this.allChildPicklistOptions = data;
    }
  }

  @wire(getRecord, {
    recordId: "$recordId",
    fields: [PARENT_PICKLIST_FIELD, CHILD_PICKLIST_FIELD],
  })
  wiredGetRecord({ data, error }) {
    if (data) {
      this.parentPicklistValue = getFieldValue(data, PARENT_PICKLIST_FIELD);
      this.filterChildPicklistOptions();
      this.childPicklistValue = getFieldValue(data, CHILD_PICKLIST_FIELD);
    }
  }

  filterChildPicklistOptions() {
    if (!this.parentPicklistValue) {
      this.childPicklistOptions = [];
      return;
    }
    this.childPicklistOptions = this.allChildPicklistOptions.filter((option) =>
      option.validFor.includes(parentValueToIndexMap[option.value]),
    );
  }

  handleParentChange(e) {
    this.parentPicklistValue = e.detail.value;
    this.filterChildPicklistOptions();
  }

  handleChildChange(e) {
    this.childPicklistValue = e.detail.value;
  }
}

And the html goes like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
  <lightning-combobox
    name="parentPicklist"
    label="Parent Picklist"
    value={parentPicklistValue}
    placeholder="Select Value"
    options={parentPicklistOptions}
    onchange={handleParentChange}
  ></lightning-combobox>
  <lightning-combobox
    name="childPicklist"
    label="Child Picklist"
    value={childPicklistValue}
    placeholder="Select Value"
    options={childPicklistOptions}
    onchange={handleChildChange}
  ></lightning-combobox>
</template>

Will this work? Sometimes yes, and sometimes no! It works when all the wire calls resolve in a particular order Fetch parentPicklistOptions -> Fetch childPicklistOptions -> Fetch record values otherwise it won’t.

This order in which the wire call getting resolved is not in our hand as per this code. But can we take control over that? That’s what we are gonna see below.

Asynchronous JavaScript to help

JavaScript Engine in itself is single threaded. But it uses browser APIs, task queues and event loop to achieve the asynchronousity (Is that a word?? Not sure, but hope you understand what I meant.). Shall we make small dive to see what it is?

Callstack

Whenever a function is called, the function is added to the call stack, and this will stay there until the function is completed, and it will be removed out of the callstack.

Have you heard of the term stack overflow? when function calling itself, without end, this callstack will be full and won’t be able to add another function to it, that when stack overflow happens.

Task queues

Task queues have list of callback functions that needed to be added to the stack for execution. Who add these callbacks to the task queue? That’s browser / runtime. Javascript handover the event listening, setTimeout, setIntervel, fetch, etc,.. to the browser. So when particular action is done, the callbacks which is associated with that will be added to the task queue.

There are two types of task queues.

  • Macrotask queue (lower priority): Event listeners, setTimeout, setInterval, etc,..
  • Microtask queue (higher priority): Promises

Event loop

Event loop, constantly check if the call stack is empty. To perform push callback functions from the task queue to the callstack for execution. That is the sole purpose of the event loop.

You can ask, Vishwa why are we reading this? Say us, how to queue the wire calls. There is a reason, see we discovered the task queues, we would be making use of the promises, to queue our wire calls and let the javascript handles the rest.

Promises in Javascript

Promises were introduced into JavaScript as part of ES6. The promise will be in the pending state and will be handled by the browser until it get resolved. Once it’s resolved the callback function then gets added to the Microtask queue. Then event loop will check the callstack is empty, if so will add that callback to the callstack for execution.

Async and Await in Javascript

How promises helps in Queueing the wire call?

We will create promises for each, and make it stay in the pending state, and resolve it in the required order manually through code. Here’s how do we do it. We will create two promises inside the constructor. The code becomes,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import { wire, api } from "lwc";
import { getRecord } from "lightning/uiRecordApi";
import { getPicklistValues } from "lightning/uiObjectInfoApi";
import PARENT_PICKLIST_FIELD from "@salesforce/schema/SC_Queue_Wire_Call__c.SC_Parent_Picklist__c";
import CHILD_PICKLIST_FIELD from "@salesforce/schema/SC_Queue_Wire_Call__c.SC_Child_Picklist__c";

export default class WireClass extends LightningElement {
  // no code changes in the front

  // created promise and resolve properties for the first two wire calls
  parentPicklistOptionsPromise;
  childPicklistOptionsPromise;
  parentPicklistOptionsResolve;
  childPicklistOptionsResolve;

  async connectedcallback() {
    super();
    this.parentPicklistOptionsPromise = new Promise((resolve, reject) => {
      this.parentPicklistOptionsResolve = resolve;
    });
    this.childPicklistOptionsPromise = new Promise((resolve, reject) => {
      this.childPicklistOptionsResolve = resolve;
    });
    this.getRecordPromise = new Promise((resolve, reject) => {
      this.getRecordResolve = resolve;
    });
  }

  @wire(getPicklistValues, {
    recordTypeId: "012000000000000AAA", // default recordtypeid
    fieldApiName: PARENT_PICKLIST_FIELD,
  })
  wiredGetParentPicklistValues({ data, error }) {
    if (data) {
      this.parentPicklistOptions = data.values;
      this.parentValueToIndexMap = data.values.reduce((acc, option, index) => {
        acc[option.value] = index;
        return acc;
      }, {});
      this.parentPicklistOptionsResolve();
    }
  }

  @wire(getPicklistValues, {
    recordTypeId: "012000000000000AAA", // default recordtypeid
    fieldApiName: CHILD_PICKLIST_FIELD,
  })
  wiredGetChildPicklistValues({ data, error }) {
    if (data) {
      this.parentPicklistOptionsPromise.then(() => {
        this.allChildPicklistOptions = data.values;
        this.childPicklistOptionsResolve();
      });
    }
  }

  @wire(getRecord, {
    recordId: "$recordId",
    fields: [PARENT_PICKLIST_FIELD, CHILD_PICKLIST_FIELD],
  })
  wiredGetRecord({ data, error }) {
    if (data) {
      this.childPicklistOptionsPromise.then(() => {
        this.parentPicklistValue = getFieldValue(data, PARENT_PICKLIST_FIELD);
        this.filterChildPicklistOptions();
        this.childPicklistValue = getFieldValue(data, CHILD_PICKLIST_FIELD);
      });
    }
  }

  // remaining same
}

The callback method inside the then will only be called when the respective promise is resolved, as we are resolving promise in particular order, we make it possible to process the data only when all the required data are retrieved.

This post is licensed under CC BY 4.0 by the author.