typescript 52 lines · 7 steps

A debounced autocomplete hook in React

A custom hook that debounces input, cancels stale requests, and returns typed suggestions.

Explained by highlit
1import { useState, useEffect, useRef, useCallback } from "react";
2 
3interface Suggestion {
4 id: string;
5 label: string;
6}
7 
8export function useAutocomplete(minLength = 2, delay = 300) {
9 const [query, setQuery] = useState("");
10 const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
11 const [loading, setLoading] = useState(false);
12 const abortRef = useRef<AbortController | null>(null);
13 
14 useEffect(() => {
15 const trimmed = query.trim();
16 if (trimmed.length < minLength) {
17 setSuggestions([]);
18 setLoading(false);
19 return;
20 }
21 
22 const handle = setTimeout(async () => {
23 abortRef.current?.abort();
24 const controller = new AbortController();
25 abortRef.current = controller;
26 setLoading(true);
27 
28 try {
29 const res = await fetch(
30 `/api/suggestions?q=${encodeURIComponent(trimmed)}`,
31 { signal: controller.signal },
32 );
33 if (!res.ok) throw new Error(`Request failed: ${res.status}`);
34 const data: Suggestion[] = await res.json();
35 setSuggestions(data);
36 } catch (err) {
37 if ((err as Error).name !== "AbortError") setSuggestions([]);
38 } finally {
39 if (abortRef.current === controller) setLoading(false);
40 }
41 }, delay);
42 
43 return () => clearTimeout(handle);
44 }, [query, minLength, delay]);
45 
46 const onChange = useCallback(
47 (e: React.ChangeEvent<HTMLInputElement>) => setQuery(e.target.value),
48 [],
49 );
50 
51 return { query, suggestions, loading, onChange };
52}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Debouncing via setTimeout with an effect cleanup avoids firing a request on every keystroke.
  2. 2An AbortController ref lets you cancel in-flight requests so slow responses never overwrite newer ones.
  3. 3Guarding state updates against the current controller keeps loading flags accurate under rapid input.

Related explainers

python
import base64
import json
from typing import Annotated, Optional
 

Cursor pagination in a FastAPI endpoint

pagination cursor async
Intermediate 9 steps
typescript
import { Body, Controller, Ip, Post, UnauthorizedException } from '@nestjs/common';
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
import { UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';

Rate-limiting an auth flow in NestJS

rate-limiting authentication guards
Intermediate 8 steps
typescript
import { Component, computed, DestroyRef, inject, input, output, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval, map, takeWhile } from 'rxjs';
 

How a signal-driven countdown works in Angular

signals reactivity rxjs
Intermediate 8 steps
typescript
type TokenType = "keyword" | "string" | "comment" | "number" | "text";
 
interface Token {
  type: TokenType;

How a regex tokenizer highlights code

tokenizer regex lexing
Intermediate 10 steps
typescript
import { Component, inject, signal } from '@angular/core';
import { CdkDragDrop, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
import { HttpClient } from '@angular/common/http';
import { finalize } from 'rxjs';

Drag-and-drop reordering with signals in Angular

drag-and-drop signals optimistic-update
Intermediate 8 steps
typescript
type Middleware<TIn, TOut> = (ctx: TIn) => Promise<TOut> | TOut;
 
class Pipeline<TIn, TOut> {
  private constructor(private readonly run: Middleware<TIn, TOut>) {}

A type-safe async middleware pipeline

generics type-safety middleware
Advanced 9 steps

Share this explainer

Here's the card — post it anywhere.

A debounced autocomplete hook in React — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code