See You Later, Generator

Scott Batson
Oct 4, 2019 · 5 min read

We are building a mobile app with NativeScript-Vue and Vuex that allows a user to search for events nearby. The user can specify what type of events they want and how far they are willing to travel. The API we integrated with accepts a radius which is the number of miles away an event could be (based on the user’s zip code).

The larger the radius, the longer this request will take. In busier areas (like large metropolitan areas), this request could take quite a while; since we were dealing with mobile devices, the user’s connection could also be somewhat slow. On top of that, we had to search multiple types of events as well as events that could apply to any location… which compounds the request time and complexity.

Ok, just add a nice loader and move on…
— People jumping to conclusions

We can’t just add a loading state and ask them to wait because the radius is controlled through a couple of plus and minus buttons. Each click would result in a new API call and if the user decides to spam the button, too many requests might go out.

Controls for our radius

Because the user can increase or decrease this radius so quickly (as well as some other controls that we won’t get into), we have two problems to solve.

  1. Throttle the update so that spamming the button only results in one execution
  2. If we start making a request and the user then changes the control again, we should be able to safely abandon the last request

Simple. setTimeout and then cancel it. Done. Ship it.
— Me after glancing at the problem

First, let’s look at what this Vuex action looks like:

This is a somewhat simplified version, but essentially, we have three total requests and each one can take a while depending on where the user is located. So… why can’t I just use a setTimeout? Couldn’t I just do:

const timer = setTimeout(store.dispatch('setEvents', params), 500);
// then some stuff happens
clearTimeout(timer);

This would mean that our action would only be executed every half second, which is a reasonable amount of time when spamming buttons.

The problem is that you can only cancel a timer before it starts. Once that function fires, it will execute from start to finish, even if those promises take a while. As far as the timer is concerned, it did its job. This is a problem because these promises are going to resolve then commit things to our store, even if they aren’t up to date anymore.

In this scenario, we could start our request, searching events within 200 miles (a request that takes a while). Then, they could spam the button and search within 25 miles (a request that doesn’t take long). In this case, the 25 miles promise resolves first and commits its payload to our store. Then, the 200 miles promise resolves and commits its content… even though it’s out of date.

Wouldn’t it be nice if we could abandon that function entirely, even if it started already?

This is when we started looking at generator functions.

Picture of a gas powered generator
Picture of a gas powered generator

If you aren’t familiar with generator functions, here are the Cliff’s Notes:

  • They allow us to iterate over a function
  • We can safely stop them and they actually stop executing

Here is the most basic example:

A generator function is something that we actually have to step through. When we call next, it returns an object with two properties. value is whatever we yielded and done is whether or not the generator function has anything left to do.

If at any time we wanted to abandon this function, we could call ourMethod.return(). Now, even if we call next(), our function won’t do anything since we told it to stop.

So, if we want to “throttle” our Vuex action, how would we do that?

  1. Wrap the whole thing in a generator function
  2. yield each promise
  3. Only call next() when the last promise has resolved
  4. If we call the function again, check to see if there is already an instance of this generator function running. If there is, call return() to stop it

The last item in this list is the trickiest part. We need a way to know which function was called last. If you read my last blog post, we came up with a way of extending the Vuex store to know what the “last action” called was. I’m going to use that here but you could easily just pass a unique string to track this yourself.

I created a separate util file that I could import. This file has 2 things:

  1. Concurrency class — this is only for saving the instance of these functions so I can cancel them
  2. throttle function — this function takes a generator function. It cancels any previously running instances of that function. Then steps through each yield block until it’s finished

I tried to leave some useful comments (which makes the file look a lot bigger than it really is).

Our throttle function waits half a second before executing our action and then steps through it, waiting for each individual promise to resolve before moving onto the next one.

Now we just wrap everything up in a generator function that is passed to our throttle method:

If our user spams that button, we will only ever execute our API requests every half second. Then, if they click the button again before any of these promises are solved, we safely stop the request, avoid committing anything incorrect to the store, and then start over.

Generator functions are great for use cases like this: debouncing and throttling functions but they can be used for so much more. Really, any time you have to iterate over something, generators are a great solution. As well as any time you have to use setTimeout but then write logic around cancelling it. Consider a generator as a more robust solution.

Writings collected from around the Upstatement office.

Thanks to Beatrice Huang and James Muspratt

Scott Batson

Written by

Currents

Writings collected from around the Upstatement office. Upstatement is a free-thinking, fun-loving creative studio that imagines & builds exceptional digital experiences. www.upstatement.com

More from Currents

More on JavaScript from Currents

More on JavaScript from Currents

More on JavaScript from Currents

Jest mocks roasting on an open Firestore

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade