System Designhard
Design an autocomplete / type-ahead component
Design and implement an autocomplete input that fetches suggestions from an API as the user types. Handle debouncing, race conditions, keyboard navigation, and accessibility.
Asked at Airbnb, Amazon, Google, Tenable
Answer
class Autocomplete { constructor(inputEl, fetchFn, options = {}) { this.input = inputEl this.fetch = fetchFn this.delay = options.delay ?? 300 this.minChars = options.minChars ?? 2 this.listEl = this._createList() this.timer = null this.activeRequest = null this.activeIndex = -1 this.input.addEventListener('input', e => this._onInput(e)) this.input.addEventListener('keydown', e => this._onKeydown(e)) this.input.setAttribute('autocomplete', 'off') this.input.setAttribute('aria-autocomplete', 'list') this.input.setAttribute('role', 'combobox') } _onInput({ target: { value } }) { clearTimeout(this.timer) if (value.length < this.minChars) return this._hide() this.timer = setTimeout(async () => { // Cancel any pending request (race condition fix) const requestId = Symbol() this.activeRequest = requestId const results = await this.fetch(value) if (this.activeRequest !== requestId) return // stale this._render(results) }, this.delay) } _onKeydown(e) { const items = this.listEl.querySelectorAll('[role="option"]') if (e.key === 'ArrowDown') { e.preventDefault() this.activeIndex = Math.min(this.activeIndex + 1, items.length - 1) } else if (e.key === 'ArrowUp') { e.preventDefault() this.activeIndex = Math.max(this.activeIndex - 1, -1) } else if (e.key === 'Enter' && this.activeIndex >= 0) { items[this.activeIndex]?.click() } else if (e.key === 'Escape') { this._hide() } items.forEach((item, i) => item.setAttribute('aria-selected', String(i === this.activeIndex)) ) } _render(items) { this.listEl.innerHTML = '' this.activeIndex = -1 items.forEach(text => { const li = document.createElement('li') li.textContent = text li.setAttribute('role', 'option') li.addEventListener('click', () => { this.input.value = text this._hide() }) this.listEl.appendChild(li) }) this.listEl.hidden = items.length === 0 } _hide() { this.listEl.hidden = true } _createList() { const ul = document.createElement('ul') ul.setAttribute('role', 'listbox') ul.hidden = true this.input.parentNode.appendChild(ul) return ul } } // Usage const inputEl = document.querySelector('#search') const fetchFn = async (q) => { const r = await fetch('/api/suggest?q=' + encodeURIComponent(q)) return r.json() } new Autocomplete(inputEl, fetchFn, { delay: 250, minChars: 2 })
Source: frontendinterviewhandbook.com — Airbnb