Ba lớp cần xử lý: giảm số request, chống kết quả về sai thứ tự, điều khiển bằng bàn phím.
Giảm request. Debounce khoảng 250–300ms và bỏ qua chuỗi quá ngắn (< 2 ký tự). Nếu có cache theo từ khoá thì gõ lại từ cũ không cần gọi API.
Kết quả sai thứ tự (race condition). Gõ "ha" rồi "han": request "ha" chậm hơn có thể về sau và ghi đè kết quả của "han". Hai cách xử lý, thường dùng cả hai:
useEffect(() => {
if (term.length < 2) return
const ctrl = new AbortController()
const t = setTimeout(async () => {
const res = await fetch(`/api/search?q=${encodeURIComponent(term)}`, { signal: ctrl.signal })
setItems(await res.json())
}, 250)
return () => { clearTimeout(t); ctrl.abort() }
}, [term])Cleanup vừa huỷ timer (chưa kịp gửi thì thôi) vừa abort request đang bay. Nếu không abort được thì so sánh: chỉ nhận kết quả khi từ khoá trả về vẫn khớp từ khoá hiện tại.
Bàn phím. Mũi tên lên/xuống di chuyển lựa chọn (preventDefault để con trỏ không nhảy trong input), Enter chọn, Escape đóng danh sách. Dùng role="combobox" cho input với aria-expanded / aria-activedescendant và role="listbox" cho danh sách để trình đọc màn hình theo kịp.
Một chi tiết dễ quên: khi request bị abort, fetch ném lỗi AbortError — cần lọc lỗi này ra, đừng hiển thị "Có lỗi xảy ra".