Harnessing the onQuery: (query: string) ⇒ void
Prop — Building a Debounced, Type‑Safe SearchBar in React 19 + TypeScript
TL;DR —
onQuery
is a callback prop that lets a parent component own what happens to the search term, while theSearchBar
child owns how the term is gathered. Combine it withuseEffect
+setTimeout
and you get an elegant, reusable, debounced search field.
1 What exactly is onQuery
?
interface Props {
placeholder?: string;
onQuery: (query: string) => void; // ← here
}
- It’s just a function—a contract that says: “Give me the latest query string, I’ll do something with it.”
Think of onQuery
like onClick
or onChange
, except you define what fires.
2 Why not fetch inside SearchBar
itself?
-
Separation of concerns —
SearchBar
cares only about UI + user events. -
Reusability — The parent decides whether to
- Hit the Giphy API
- Update URL search params
- Trigger state in Redux / Zustand
-
Testability — Pass a jest mock and assert it’s called with
"cat"
.
3 Dissecting the Component
import { useEffect, useState, type KeyboardEvent } from 'react'
interface Props {
placeholder?: string;
onQuery: (query: string) => void; // ← here
}
export const SearchBar = ({ placeholder = 'Buscar', onQuery }: Props) => {
const [query, setQuery] = useState('');
// 1️⃣ Debounce side‑effect
useEffect(() => {
const id = setTimeout(() => onQuery(query), 700);
return () => clearTimeout(id);
}, [query, onQuery]);
const handleSearch = () => onQuery(query); // 2️⃣ Immediate search
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => { // 3️⃣ Enter key
if (e.key === 'Enter') handleSearch();
};
return (
<div className="search-container">
<input
type="text"
placeholder={placeholder}
value={query}
onChange={e => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
/>
<button onClick={handleSearch}>Buscar</button>
</div>
);
};
3.1 Debounce with useEffect
Render cycle | What happens |
---|---|
User types “c” |
query → "c" → schedules timer #1 |
User types “ca” | Clears timer #1, schedules timer #2 |
Pause ≥ 700 ms | Timer fires → onQuery("cat")
|
3.2 Immediate search
Click or Enter bypass the debounce for instant feedback.
4 Parent‑Side Usage
export function GifApp() {
const [results, setResults] = useState<Gif[]>([]);
// Stable callback prevents needless re‑effects
const handleQuery = useCallback((q: string) => {
fetchGifs(q).then(setResults);
}, []);
return (
<>
<SearchBar onQuery={handleQuery} />
<GifList gifs={results} />
</>
);
}
React 19 tip: Wrapping
handleQuery
inuseCallback
keeps its reference stable soSearchBar
’s effect only reruns whenquery
changes.
5 Design Patterns & Best Practices
Pattern | Why it rules |
---|---|
Lift state up | UI remains dumb; parent decides side‑effects |
Stable callbacks |
useCallback prevents re‑debounce storms |
Effect cleanup | Avoid memory leaks when component unmounts |
Typed callback props | Compile‑time guarantee of correct usage |
Trim empty queries |
if (!q.trim()) return; inside handler |
6 Extending the Pattern
6.1 Result count indicator
<SearchBar count={results.length} onQuery={handleQuery} />
6.2 onClear
hook
interface Props {
onQuery: (q: string) => void;
onClear?: () => void;
}
6.3 Smooth UX with useTransition
const [isPending, start] = useTransition();
const handleQuery = (q: string) =>
start(() => setSearch(q)); // typing stays snappy
7 Cheat‑Sheet
Need | Code / Concept |
---|---|
Debounce |
setTimeout + cleanup |
Keep callback stable | useCallback |
Trigger on Enter |
onKeyDown + event.key === 'Enter'
|
Avoid first empty call | Initialise state as ''
|
Cancel fetch |
AbortController inside useEffect
|
Final Thoughts
onQuery
turns a tiny input into a reusable query engine nozzle.
Master typed callback props, debounced effects, and controlled inputs to build search UIs that feel instant and stay maintainable.
Happy querying — and may your API responses be ever in your favour! 🚀
Top comments (0)