Handling Asynchrony with Vue Composition API and vue-concurrency

Part 2 — canceling, throttling, debouncing, polling

Martin Malinda
JavaScript in Plain English

--

Visualization of a restartable task in vue-concurrency

In the previous article, I talked about promises and handling async state. This article will point towards another weak point of promises: lack of cancelation.

It’s easy to think that you have no need for cancelation. Paradoxically, because with the current default tools in JavaScript cancelation is hard, it’s not often talked about and recommended. But there indeed are use cases when cancelation is a powerful tool and not just for some advanced use cases, but also in common features such as debouncing and polling or just to make asynchronous logic overall more safe. You just need the right tool.

Promises are not cancellable and therefore you also can’t cancel an async function. One way to go around this is to make use of generators. Generator functions, like async functions, have a special syntax.

function myFun * () {
yield foo();
return 'bar';
}

Instead of async we mark the function with * and instead of await we write yield . But then generator functions are more flexible — their behavior is to a big degree defined by the consumer they are passed into. You can create yuor own handler of generator functions but most commonly you’d just rely on a 3rd party library. For example CAF — Cancellable Async Flows. Generator functions passed into CAF start to behave like async functions — yielding waits for promise resolution. But on top of that, it’s possible to cancel() and abort the execution early.

vue-concurrency takes use of CAF and wraps the whole logic into a handy reactive object: Task.

Polling

A typical use case when the cancelation is very handy: polling. When you poll, you do a certain operation in a specified interval for a long time or until a specific action. The most direct approach would be using setInterval but then you need to make sure to also clearInterval at the right time. After all, your Vue app is probably behaving like a single page application and you don’t want your polling to continue when the user navigates to a different page for example. So yes you could do a few defensive checks and then clear interval in onBeforeUnmount composition API hook. But with vue-concurrency it gets a bit more straightforward:

The while (true)is something that’s rarely seen in JavaScript. But with cancellation it becomes viable. Poll task waits for the new data, then waits 5 seconds over and over again. The task is canceled automatically when the component where it is used is unmounted. So this code is safe to do, no need to deal with setInterval.

Right away on the task, there’s also the drop() modifier. It makes things a bit more bullet-proof, making sure that task can’t run multiple instances at the same time. In this case, it means polling can only be running once. If the task might need to start again, but with different arguments, perhaps checking a different endpoint, it could be set as restartable() instead. A restartable task would cancel the previous task instance and start a new one.

And that’s it! If needs be, we can also update our code further to allow canceling and resuming manually:

As we pass the task to the template, we get the usual benefits of it. In the template we can check if task.isRunning and we can resiliently display lastSuccessful value (assuming some API calls might fail):

Debouncing

Showing search results as the user has finished typing is a common feature nowadays and provides a good UX. The direct approach is to trigger a timer after a keypress and reset the timer if it’s already running. And if yo u wouldn’t write this logic yuorself, you’d generally just wrap your function in some existing debounce utility, for example from lodash.

With vue-concurrency and tasks, the operation again gets a bit more straightforward and safe.

We can create a task that is similar to the previous one:

The difference here is that timeout() is at the beginning of the task call. We wait for 700ms and then perform an AJAX request. If the task is performed again (and it will be for every keypress), the previous task call is canceled and a new task instance is created because the task is restartable() . That effectively gives us debouncing. This makes things less magical than using a mysterious debounce function and it also gives you more flexibility. If necessary, the waiting time could be dynamic (perhaps not waiting on certain keys!… or waiting longer).

Throttling

What if we want to show search results while user is typing, not after? In that scenario, it’s still probably not doable to perform a search for every key press. We want to limit the search API calls a certain way. That’s where throttling comes into play. Let’s say we want to search as the user is typing, but only every 200ms. We could utilize the drop() modifier from the first (polling) example but it wouldn’t be ideal. keepLatest() is better, fit. Just like drop() it also prevents the task to run multiple instances in parallel, but it makes sure the last instance is eventually performed. This is ideal for throttling.

Yes, the only difference here is the different number passed to timeout and using keepLatest() instead of restartable() !

Up next

Not literally part of this series but I have one more article that fits into this area: building a basic data layer:

Subscribe on herohero for weekly coding examples, hacks and tips

Hey 👋 If you find this content helpful, subscribe to me on herohero where I frequently share concise and useful coding tips from my day-to-day experience working with JavaScript and Vue.

--

--