Components With Async Friendly Event Handlers

Posted on May 26, 2021

All apps have to listen to user input, it could be a button click or some text being typed in an input box. These user actions in return perform a task like api call, validating the input, etc. These actions can be asynchronous in nature and it's in these cases where having async event handlers is quite beneficial.

Let's take a simple example of a button. On clicking the button, we'll make an api call and then render an image from the api's response. I'll be using React here but these ideas remain same for other frameworks too.

As you can try in the above example it works perfectly well. But what if it's an expensive api call (time consuming) or if the user is on a bad network? There are a few issues with this scenario. Let's look at them one by one:

Writing Components With Async Event Handler

Let's look at the same example but this time implemented with async onClick handler.

What did we change? We made the onClick function passed to button async function which resolves after the response from the api call. We also added a Button component which has it's own internal state which enables the button or shows the 'loading' state. The Button component can internally change this state based on when it's clicked and when the onClick handler resolves.

Benefits Of Using Async Event Handler

Here we considered a button component but, this same logic can be extending to all forms of input components.

Imagine you have a input box with some async validations. How nice it'd be, if the input component can update it's state (show user the validation error) based on whether the async onChange is resolved or rejected. We can even reject it with the custom validation error message for input component to show!

Adding Compatibility For Sync OnClick Handlers

We may still want to use the Button component for non-async tasks. One way is to convert all your functions to async function by simple using the async keyword.

async function asyncFunctionDoingSyncTask() {
    // some synchronous code here

Another way is to modify our Button component to accept both kinds of onClick handler functions - sync and async. We can do this simply by wrapping the response of onClick handler in Promise.resolve. This will return a promise if the onClick handler was not an async function. Doesn't matter if it's already returning a Promise.

// set Button state to 'loading'
let response = onClickHandler();
response = Promise.resolve(response);
await response;
// set Button state to 'done'

Some may argue that this will make a synchronous code execute in an asynchronous way as we make use of Promise which will add execution after await to microtask queue. You may use instanceof to determine if the return value of the onClick handler is a Promise or not.

const response = onClickHandler();
if (response instanceof Promise) {
    response.then(() => {
        // set Button state to done
} else {
    // set Button state to done

Hope you found it helpful! Let's write more components with async friendly event handler :)