#Async#Keyboard

Typeahead With Keyboard Navigation

Search-as-you-type with debouncing, request cancellation, arrow-key navigation and the combobox roles.

By Pratik RaiMedium

A city search that queries as you type once at least three characters are entered, with arrow keys to move through results and Enter to select.

The search function here has deliberately variable latency, and that is the point. Most candidates debounce, see a working search, and stop — but with a debounce alone a slow request for lon can resolve after a fast one for lond and replace the correct results with stale ones. It is a real production bug, it is invisible on a fast connection, and it is exactly why this gets asked.

So two mechanisms are doing two jobs. The timer reduces how many requests you make. An AbortController makes sure a request whose query is no longer current cannot affect state at all. Both are undone in the same effect cleanup, which is what makes the pairing natural: React tears down the previous effect before running the next, so "cancel whatever the last keystroke started" needs no extra bookkeeping.

The alternative to aborting is a staleness guard — capture the query, compare it on resolve before calling setState. It works, and it is the right answer where cancellation is not available. Aborting is better where it is, because it stops the work rather than discarding it.

The subtle one is the selection loop: writing the chosen value into the input changes the query, so the effect reruns and reopens the dropdown over the choice just made. A ref checked at the top of the effect breaks the cycle.

Goal: Search after three characters, navigate by keyboard, and never show a stale response.

Source

Frequently asked questions

Isn't debouncing enough for a search-as-you-type?
No. Debouncing reduces how many requests you make but does nothing about one already in flight. A slow earlier response can resolve after a faster later one and overwrite the correct results.
How do you stop a stale response overwriting newer results?
Abort the previous request when the query changes, in the effect cleanup. Where cancellation is not available, capture the query and compare it on resolve before calling `setState`.
Why does selecting a result retrigger the search?
Because filling the input changes the query, so the effect keyed on it runs again and reopens the list over the choice just made. A ref set immediately before lets the next run skip itself.
What makes a typeahead accessible?
The input is a `role="combobox"` with `aria-expanded`, the results are a `role="listbox"` of options with `aria-selected`, and `aria-activedescendant` points at the highlighted option so movement is announced while focus stays in the input.

Related Challenges

Continue learning with these related challenges

View All
React

Accessible Dropdown

A select built from scratch — keyboard navigation, focus management, outside-click and the full ARIA listbox contract.

React · JavaScriptPratik Rai ·

React

Image Carousel

Create an interactive image carousel in React with smooth slide transitions, navigation arrows, dot indicators, autoplay, and touch/swipe support for mobile devices.

React · JavaScriptPratik Rai ·

React

Dynamic Tic Tac Toe

Build a dynamic Tic Tac Toe game in React with customizable grid sizes, win detection algorithms, player turn management, and game reset functionality. Great for interviews.

React · JavaScriptPratik Rai ·