Search as you type at 60fps with js-coroutines
Mike Talbot โญ
Posted on July 6, 2020
It's nice to be able to make user interfaces that require the least number of clicks for the user to achieve their goal. For instance, we might want to search a list as we type. The challenge is though, as the list gets bigger there's a chance that the whole user experience will degrade as our JavaScript hogs the main thread stopping animations and making the whole experience glitchy.
This article will show how we can quickly modify a standard search function to use js-coroutines and keep the fully responsive experience with very little extra effort.
Let's say we have a list of 1,000,000 items and we have a text box, as the user types, we'd like to return the first 50 entries that have words that match the words they've typed (in any order).
For this example, we'll use "unique-names-generator" to create a list of nonsense to search on! Entries will look a little like this:
Aaren the accused lime flyingfish from Botswana
Adriana the swift beige cuckoo from Botswana
Our search function is pretty simple:
function find(value) {
if (!value || !value.trim()) return []
value = value.trim().toLowerCase()
const parts = value.split(" ")
return lookup
.filter(v =>
parts.every(p =>
v.split(" ").some(v => v.toLowerCase().startsWith(p))
)
)
.slice(0, 50)
}
But with 1,000,000 entries the experience is pretty woeful. Try searching the in the screen below for my favourite dish: 'owl rare', and watch the animated progress circle glitch...
This experience is atrocious and we'd have to either remove the functionality or find a much better way of searching.
js-coroutines to the rescue!
With js-coroutines we can just import the filterAsync
method and re-write our "find" to be asynchronous:
let running = null
async function find(value, cb) {
if (running) running.terminate()
if (!value || !value.trim()) {
cb([])
return
}
value = value.trim().toLowerCase()
let parts = value.split(" ")
let result = await (running = filterAsync(
lookup,
v =>
parts.every(p =>
v.split(" ").some(v => v.toLowerCase().startsWith(p))
)
))
if (result) {
cb(result.slice(0, 50))
}
}
Here you can see we terminate any currently running search when the value changes, and we've just added a callback, made the function async and that's about it.
The results are much better:
Posted on July 6, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 18, 2024