Demystifying “flushPromises”. Learn why you need flushPromises | by Suraj Pillai | Nov, 2022

Learn why you need FlushPromise

photo by tatiana shishkina Feather unsplash

The title here is an ode to the popular flushPromises The method you most often see in LWC tests can take one of the following two forms:

function flushPromises()
return Promise.resolve();

function flushPromises()
return new Promise((resolve) =>
setTimeout(resolve, 0);
);

If you’ve ever wondered what’s the difference between the two forms or why do you sometimes need several flushPromises either Promise.resolve To pass your prank tests, we have to get down in the weeds a bit here. This might also be an opportunity to solidify your understanding of JavaScript’s non-blocking structure, so none of its async magic ever eludes you. Well, for the most part. So, gear up.

If you’ve done any frontend development in the past few years, ever writing an LWC, chances are you’ve come across Promises, async/await, setTimeout, and fetch APIs in JavaScript. The common thread between all of these is the fact that they are all asynchronous function invocations of some sort.

In other words, these apps allow you to defer the invocation of a function and not block the main thread, or just the thread, because JavaScript is single-threaded. The way the JavaScript runtime handles delayed method execution is through an event loop.

The event loop in JavaScript gets its name because it is an infinite loop waiting for new messages to queue. When it receives a new message, it pushes it onto the call stack to be processed. These can be XHR requests, callbacks from promises, setTimeout callbacks, event listeners, and more. Some things to note here are that there are multiple message queues with different priorities in which the event loop checks for messages and the runtime processes each message in its entirety before moving on to the next message.

js runtime visual representation showing heap, stack and queue
Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop

Some of you may be wondering: if JavaScript is single threaded, how does it track when a timer setTimeout Callbacks can be pushed to message queues when the call has ended or when a network call has completed?

The simple answer is that it is not. It’s the browser itself that has other non-JS threads dedicated to this purpose. The browser provides APIs for JavaScript to communicate with it which are collectively referred to as Web APIs.

For our discussion, we’ll focus on two messages that the event loop can process: tasks and microtasks. Have messages queued using microtasks Promise, Async,Await, get api and mutation observer. task callbacks are scheduled in any other way, in particular usingsetTimeout,

Consider the following passage:

setTimeout(()=>console.log("Timeout callback"),0); // Task
Promise.resolve()
.then(()=>console.log("Promise callback")); // Microtask
console.log("I'm synchronous");

/** Output
I'm synchronous
Promise callback
Timeout callback
**/
/** Snippet 1 **/

As expected, the synchronous code is executed first. However, even though setTimeout callback was previously scheduled, we see Promise Callback executed first. This is because microtasks have a higher priority than tasks. As an LWC developer this is important to understand and remember. Allow me to explain why.

LWC properties are reactive. In other words, when you change the value of a property and if it is referenced directly or indirectly (via getters) in your template markup, the markup will be updated by the LWC runtime to reflect the property’s updated value. is presented again.

This is completely out of the developer’s control, ie, there is no way to prevent this from happening or to fix it exactly when you want the template to be reevaluated. Reevaluation is scheduled as a microtask when you change a reactive property in a JS function in your LWC, meaning that the reevaluation will only happen when your function has completed its execution.

****** js file*****

myVal = 0;
changeVal()
this.myVal=1;
this.myVal=2; //the template will be re-evaluated only after this step which means you will never see the value '1' on the page and the step above is redundant.

/** Snippet 2 **/

Let us consider another implication of this fact. Here’s the code:

****** jest test *****

it("renders button when prop is set",()=>
//initialize and add component to DOM here

element.renderButton=true; //'element' refers to LWC under test
const btn = element.shadowRoot.querySelector('lightning-button');
expect(btn).toBeNull();
)
/** Snippet 3 **/

If you expect the button to be rendered immediately after setting the relevant properties as shown in the above snippet, you would be wrong. The correct way to handle the newly added button would be to refactor our code:

Promise.resolve().then(()=>
const btn = element.shadowRoot.querySelector('lightning-button');
expect(btn).not.toBeNull(); //we've a reference!
);
/** Snippet 4 **/

This works because when we add our snippet behind a Promise.resolve call, it is scheduled as a microtask. This microtask is executed only after the template recompilation microtask, which was already scheduled by the LWC runtime. So the template holds the button until the time our callback runs. Note that you can use async/await instead PromiseIn a more linear code style.

Another thing to note is that waiting for template recompilation only happens when the template changes in response to a reactive property change. If you manipulate the DOM directly, changes can be “seen” immediately. For example, the following test will pass:

import LightningButton from "lightning/button";
it("adds button synchronously",()=>
// setup "element" LWC and add it to the DOM
const btn = createElement("lightning-button", is: LightningButton );
btn.classList = "jest";
element.appendChild(btn); //add directly to the DOM
let theBtn = element.shadowRoot.querySelector("lightning-button.jest");
expect(theBtn).not.toBeNull(); //no `Promise.resolve` needed!
);
/** Snippet 5 **/

Going back to the comparison between tasks and microtasks, as shown above (snippet 1), microtasks have a higher priority than tasks. Let’s examine some of the implications of this fact with the following code:

/*** component.html ***/

/*** component.js ***/

showDiv=true;
@api
unhide()
this.showDiv=true;

/** jest test **/

it("renders div when method is called",()=>
//create component and add it to DOM
element.unhide();
Promise.resolve().then(()=>
let div = element.shadowRoot.querySelector("div.dyna");
expect(div).not.toBeNull();
)
);

/** Snippet 6 **/

As seen in snippet 4, adding a Promise.resolve Allows rendering microtasks to be executed in response to changes called in the Jest test showDiv property when unhideIt is called As a result, we see the test passed. Now consider a change:

/*** component.html ***/

/*** component.js ***/

showDiv=true;
@api
unhide()
// the prop is changed only after the apex call returns
someApexMethod().then(result=>
this.showDiv=true;
);

/** jest test **/

it("renders div when method is called",()=>
//create component and add it to DOM
element.unhide();
Promise.resolve().then(()=>
let div = element.shadowRoot.querySelector("div.dyna");
expect(div).not.toBeNull(); //fails!!
)
);

/** Snippet 7 **/

Here, instead of immediately changing showDiv property, a top method is called first. After the method returns, we set the property to true to render the div. So why does the test fail? it fails because now unhideThe method has to resolve the actual rendering microtask in response to the promise of the Apex method and then setting showDiv before the element appears in the template.

Note that in tests, we usually have top methods mocking hard-coded result and hence, they are resolved immediately. So, calling the top method is, effectively, a Promise.resolve Call.

when we add a Promise.resolve call in jest test, for that callback is executed after callback someApexMethod in component but before microtask for re-evaluation of template in response to change showDiv , A simple example to illustrate this is shown below:

Promise.resolve().then(()=>console.log(1)).then(()=>console.log(2));
Promise.resolve().then(()=>console.log(3)).then(()=>console.log(4));

/** output **/
1
3
2
4

/** Snippet 8 **/

One way to make the test pass is to add a microtask to push the test forward, as shown below:

it("renders div when method is called",()=>
//create component and add it to DOM
element.unhide();
Promise.resolve()
.then(()=>) //empty function
.then(()=>
let div = element.shadowRoot.querySelector("div.dyna");
expect(div).not.toBeNull(); //passes
)
);

/** Snippet 9 **/

What if there are multiple mandatory Apex method calls in the component method before the Reactive property is set? you have to keep adding empty.then calls in your test to push your assertions back far enough so that the recompilation of the template can complete. Unless you’re testing intermediary states between apex method calls, this makes your testing unnecessarily verbose in my opinion. Since the goal of your test is to make sure the element has been added to the template don’t worry about how many callbacks it needs for that to happen. Here is an option to defer your inserts using setTimeout As shown in the snippet below:

it("renders div when method is called",()=>
//create component and add it to DOM
element.unhide();
setTimeout(()=>
let div = element.shadowRoot.querySelector("div.dyna");
expect(div).not.toBeNull(); //passes
,0);
);
/** Snippet 10 **/

regardless of the number of apex methods unhide, we only need one setTimeout Call to defer our assertions because all Resolvable Promise callbacks (microtasks) are executed before the event loop picks up the assert task. Note that we don’t even need to add a delay to our setTimeout Call!

The last thing I want to cover here is event handlers. Consider the following snippet, where we have a button and a message on the screen. js file, we register two event handler functions on the same button. Here’s what it looks like:



message

/*** JS ***/

_message = "Hello"; │

@api │
get message() │
return this._message; │

reset() │
this._message = "Hello"; │

_handleClickOne = (event) => │
console.log(">>. in click handler one "); │
this.reset(); │
// event && event.preventDefault() && event.nativeEvent.stopImmediatePropagation() && event.stopPropagation(); │
this._message += " One"; │
Promise.resolve().then(() => (this._message += " Promise")); │
return false; │
; │

_handleClickTwo = () => │
console.log(">>. in click handler two "); │
this._message += " Two"; │
;

/** We are registering two event handlers on the same button **/

_addEventHandlers() │
const btn = this.template.querySelector(".btn"); │
btn.addEventListener("click", this._handleClickOne); │
btn.addEventListener("click", this._handleClickTwo); │


renderedCallback() │
if (this._listenersAdded) return; │
this._listenersAdded = true; │
this._addEventHandlers(); │
console.log("listeners added"); │

/**** Jest Test ***/

it("it should change the message when button is clicked", async () =>
// component setup
await Promise.resolve(); //wait for template evaluation
element.shadowRoot.querySelector(".btn").click();
await Promise.resolve(); //wait for the Promise in the event handler to e resolved
expect(element.message).toBe("Hello One Two Promise"); //passes
);

/** Snippet 11 **/

The results seem simple enough. Initially, you click on the button, event listeners are called one after the other, and finally, Promise The callback has been executed. The last message you see on the screen as well as your mock test Hello One Two Promise, Now, let’s modify this slightly to use a standard HTML button instead. lightning-button. Everything else remains the same.

/** Snippet 12 **/

You will find that the test still passes. But when you add this component to a page and click the button, guess what happens when you click the button?

the message you get reads Hello One Promise Two , You don’t have to take my word for it. Try it yourself.

This is because standard HTML event listeners are executed as functions in the order they are registered when triggered from a web page.

Applying this to the above example, _handleClickOne It is called first. Within this method the promise callback is added to the microtask queue. next event listener _handleClickTwo is added as a task to be executed in the next event loop iteration. However, since the microtask has a higher priority than the tasks seen above, Promise callback is executed first handleClickTwo , When the event is triggered from JavaScript as opposed to the UI, such as by calling button.click()All registered event listeners are executed synchronously instead of tasks.

this is how we see _handleClickOne And _handleClickTwo executed synchronously before Promise Callbacks are executed as microtasks. to clicklightning-button The UI also causes event listeners to execute synchronously, which is different from the behavior of standard HTML buttons. Now whether that is by design or a bug is anyone’s guess. At any rate, it can be helpful to be aware of this discrepancy if you encounter such a situation.

Hopefully, if you’ve made it this far, you’ve been able to gather a few things about Event Loops, Tasks and Microtasks and their relevance in LWC development.

If you’re interested in going further down this rabbit hole, here is the most popular video On Event Loops on YouTube. is here Another by Googler Which has some better visualizations.

Leave a Reply