Harnessing the onQuery: (query: string) ⇒ void Prop — Building a Debounced, Type‑Safe SearchBar in React 19 + TypeScript
TL;DR —
onQueryis a callback prop that lets a parent component own what happens to the search term, while theSearchBarchild owns how the term is gathered. Combine it withuseEffect+setTimeoutand 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 —
SearchBarcares 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
handleQueryinuseCallbackkeeps its reference stable soSearchBar’s effect only reruns whenquerychanges.
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)