Convert On Click To Promise
Sometimes we find libraries or built-in functionality that use a callback pattern. Sometimes, you might want to rewrite the callback pattern into a Promise.
Why?
Sometimes we find libraries or built-in functionality that use a callback pattern, and when we use it, the code leaves a bad smell. You might want to rewrite the callback pattern into a Promise one in this situation.
In my opinion, Promises with “async / await” are more readable and preferable to callbacks.
Example
Let’s look at a simple example of a familiar “Show More” button at the end of a list.
The quotes come from "Ron Swanson API".
If we write this with the event listener, we might have something like the following:
// Global variable
let currentPage = 0;
button.addEventListener("click", async () => {
const response = await fetch(`some-url/?page=${page}`);
const quotes = await response.json();
renderQuotes(quotes);
// If we don't update global variable, we'll fetch always the same page
currentPage += 1;
})
It’s a little ugly to rely on a global variable to keep track of the current page. So let’s rewrite this with a Promise and “async / await.”
The following is the code that I’d like to have by the end of this refactor:
const fetchMore = async (page = 1) => {
await button.waitForClick();
const response = await fetch(`some-url/?page=${page}`);
const quotes = await response.json();
renderQuotes(quotes);
fetchMore(page + 1);
};
Notice the await button.waitForClick();
. This is the key to this example. The execution stops there, and it doesn’t continue until the user clicks the button.
Let’s implement this.
Step 1: Add “waitForClick” to HTMLElement class
First of all, we need to add the possibility of calling a method called “waitForClick” on HTML Elements. This method is available only on our website.
One way of doing this is by adding it to the HTML prototype.
HTMLElement.prototype.waitForClick = function() {
console.log('Extending HTML element');
};
To learn more about prototype chains, I recommend this article by MDN.
Step 2: Add the click listener
Next, we need to add a listener when the user clicks the button inside the “waitForClick” method:
HTMLElement.prototype.waitForClick = function() {
// “this” is the actual html element
this.addEventListener("click", () => {
console.log('in da click');
})
};
Every time the user clicks, we get “in da click” printed in the console.
Step 3: Return a promise
Here comes the trickiest and coolest part of this article (IMHO):
HTMLElement.prototype.waitForClick = function() {
return new Promise((resolve) => {
this.addEventListener("click", () => {
resolve();
});
});
};
Now, “waitForClick” returns a Promise. Inside that Promise, we set the listener. Why is that?
To finish the Promise, we need to call the “resolve” function. The “resolve” function is the first parameter in the callback to new Promise(callback)
. This “callback”, receives two parameters: the first is the “resolve,” and the second one is a function to “reject” the Promise. For more details, the MDN article on Promises is very good.
This was also our last step. Now, we can use waitForClick
with the Promise pattern:
button
.waitForClick()
.then(() => {
console.log(‘button was clicked’)
});
But this looks way better with the “async / await” pattern:
const exampleFunction = async () => {
await button.waitForClick();
console.log(‘button was clicked’);
};
Conclusion
With the implementation of “waitForClick”, our desired code now works:
const fetchMore = async (page = 1) => {
await button.waitForClick();
const response = await fetch(`some-url/?page=${page}`);
const quotes = await response.json();
renderQuotes(quotes);
fetchMore(page + 1);
};
Even though this seems like an infinite loop, it isn’t. The execution stops at button.waitForClick
, and it doesn’t continue until the user clicks on the button.
And the key here is that the Promise resolves when the element is clicked:
return new Promise((resolve) => {
this.addEventListener("click", () => {
resolve();
});
});
Converting a callback pattern to a Promise might make your code more readable and therefore cleaner and more maintainable.
If you like this post, consider sharing it with your friends on twitter or forwarding this email to them 🙈
Don't hesitate to reach out to me if you have any questions or see an error. I highly appreciate it.
And thanks to Miquel for reviewing this article 🙏