Search Input Overview
Real-time Search
Search while typing Immediate results
Autocomplete
Suggested search terms Quick selection
Debounce
API call optimization Remove unnecessary requests
Highlighting
Search term emphasis Improved readability
What is Search Input?
Problem: Limitations of Basic Search
The basic HTML<input type="search"> requires clicking a search button to execute the search.
// ❌ Traditional search - inconvenient
function SearchPage() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
async function handleSearch(e: React.FormEvent) {
e.preventDefault();
const { results } = await searchService.search({ query });
setResults(results);
}
return (
<form onSubmit={handleSearch}>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button type="submit">Search</button>
</form>
);
}
- Button click required: Need to press enter or button to search
- Delayed feedback: Need to execute search to see results
- No autocomplete: User has to type everything
- Hard to fix typos: Can’t discover typos until seeing results
Solution: Search Input Component
Sonamu’s Search Input provides real-time search while typing and autocomplete.// ✅ Real-time search - convenient
<SearchInput
onSearch={(query) => {
// Automatically called while typing
}}
placeholder="Search..."
showSuggestions
/>
- ✨ Immediate feedback: See results while typing
- ✨ Autocomplete: Quick input with suggested search terms
- ✨ Debounce: Minimize unnecessary API calls
- ✨ Keyboard navigation: Select suggestions with arrow keys
Basic Usage
Simple Search
The most basic search functionality.import { SearchInput } from "@/components/common/SearchInput";
import { useState } from "react";
function SearchPage() {
const [query, setQuery] = useState("");
return (
<div>
<SearchInput
value={query}
onChange={setQuery}
placeholder="Search posts..."
/>
{query && (
<div>Searching for: {query}</div>
)}
</div>
);
}
With Autocomplete
Display suggestion items.function SearchWithSuggestions() {
const [query, setQuery] = useState("");
const [suggestions, setSuggestions] = useState<string[]>([]);
// Load suggestions on query change
useEffect(() => {
if (query) {
searchService.getSuggestions({ query }).then(({ suggestions }) => {
setSuggestions(suggestions);
});
} else {
setSuggestions([]);
}
}, [query]);
return (
<SearchInput
value={query}
onChange={setQuery}
suggestions={suggestions}
onSelectSuggestion={(suggestion) => {
setQuery(suggestion);
// Execute search with selected suggestion
}}
placeholder="Type to search..."
/>
);
}
Integration with Service
Search using Sonamu Service.import { searchService } from "@/services/search.service";
import { useDebounce } from "@/hooks/useDebounce";
function SearchPage() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const debouncedQuery = useDebounce(query, 300);
// Search with debounced query
useEffect(() => {
if (debouncedQuery) {
searchService.search({ query: debouncedQuery }).then(({ results }) => {
setResults(results);
});
} else {
setResults([]);
}
}, [debouncedQuery]);
return (
<div>
<SearchInput
value={query}
onChange={setQuery}
placeholder="Search..."
/>
<div className="results">
{results.map((result) => (
<div key={result.id}>{result.title}</div>
))}
</div>
</div>
);
}
Advanced Features
Entity-specific Search
Search specific entities.function EntitySearch() {
const [entityType, setEntityType] = useState<"users" | "posts">("users");
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
useEffect(() => {
if (query) {
const service = entityType === "users" ? userService : postService;
service.search({ query }).then(({ results }) => {
setResults(results);
});
}
}, [query, entityType]);
return (
<div>
<div className="entity-selector">
<button
className={entityType === "users" ? "active" : ""}
onClick={() => setEntityType("users")}
>
Users
</button>
<button
className={entityType === "posts" ? "active" : ""}
onClick={() => setEntityType("posts")}
>
Posts
</button>
</div>
<SearchInput
value={query}
onChange={setQuery}
placeholder={`Search ${entityType}...`}
/>
<div className="results">
{results.map((result) => (
<div key={result.id}>
{entityType === "users"
? result.username
: result.title
}
</div>
))}
</div>
</div>
);
}
Integration with Filters
Apply filters along with search terms.function AdvancedSearch() {
const [query, setQuery] = useState("");
const [filters, setFilters] = useState({
category: null,
dateRange: null,
author: null,
});
const { results, isLoading } = useSearch({
query,
...filters,
});
return (
<div>
<div className="search-bar">
<SearchInput
value={query}
onChange={setQuery}
placeholder="Search posts..."
/>
</div>
<div className="filters">
<select
value={filters.category || ""}
onChange={(e) => setFilters({ ...filters, category: e.target.value || null })}
>
<option value="">All Categories</option>
<option value="tech">Tech</option>
<option value="news">News</option>
</select>
</div>
<div className="results">
{isLoading ? (
<div>Loading...</div>
) : (
results.map((result) => (
<div key={result.id}>{result.title}</div>
))
)}
</div>
</div>
);
}
Search History
Save and display recent search terms.function SearchWithHistory() {
const [query, setQuery] = useState("");
const [history, setHistory] = useState<string[]>([]);
// Load history from local storage
useEffect(() => {
const saved = localStorage.getItem("search-history");
if (saved) {
setHistory(JSON.parse(saved));
}
}, []);
function handleSearch(searchQuery: string) {
// Add to history
const newHistory = [searchQuery, ...history.filter(h => h !== searchQuery)].slice(0, 10);
setHistory(newHistory);
localStorage.setItem("search-history", JSON.stringify(newHistory));
// Execute search
setQuery(searchQuery);
}
return (
<SearchInput
value={query}
onChange={setQuery}
onSearch={handleSearch}
suggestions={query ? [] : history} // Show history when no input
placeholder="Search..."
/>
);
}
Highlighting
Highlight search terms in results.function SearchResults({ results, query }: {
results: any[];
query: string;
}) {
function highlightText(text: string, highlight: string) {
if (!highlight) return text;
const parts = text.split(new RegExp(`(${highlight})`, "gi"));
return parts.map((part, i) =>
part.toLowerCase() === highlight.toLowerCase() ? (
<mark key={i}>{part}</mark>
) : (
part
)
);
}
return (
<div>
{results.map((result) => (
<div key={result.id}>
<h3>{highlightText(result.title, query)}</h3>
<p>{highlightText(result.content, query)}</p>
</div>
))}
</div>
);
}
Custom Hook Patterns
useSearch Hook
Abstract search logic into a reusable Hook.// hooks/useSearch.ts
import { useState, useEffect } from "react";
import { useDebounce } from "./useDebounce";
export function useSearch<T>(searchFn: (query: string) => Promise<T[]>, debounceMs: number = 300) {
const [query, setQuery] = useState("");
const [results, setResults] = useState<T[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const debouncedQuery = useDebounce(query, debounceMs);
useEffect(() => {
if (!debouncedQuery) {
setResults([]);
return;
}
setIsLoading(true);
setError(null);
searchFn(debouncedQuery)
.then(setResults)
.catch((err) => setError(err.message))
.finally(() => setIsLoading(false));
}, [debouncedQuery]);
return {
query,
setQuery,
results,
isLoading,
error,
};
}
function SearchPage() {
const { query, setQuery, results, isLoading } = useSearch(
(q) => postService.search({ query: q }).then(r => r.posts)
);
return (
<div>
<SearchInput
value={query}
onChange={setQuery}
placeholder="Search posts..."
/>
{isLoading ? (
<div>Searching...</div>
) : (
<div>
{results.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)}
</div>
);
}
useSearchSuggestions Hook
Manage autocomplete suggestions.// hooks/useSearchSuggestions.ts
import { useState, useEffect } from "react";
import { useDebounce } from "./useDebounce";
export function useSearchSuggestions(
getSuggestionsFn: (query: string) => Promise<string[]>,
minLength: number = 2,
) {
const [query, setQuery] = useState("");
const [suggestions, setSuggestions] = useState<string[]>([]);
const debouncedQuery = useDebounce(query, 200);
useEffect(() => {
if (debouncedQuery.length < minLength) {
setSuggestions([]);
return;
}
getSuggestionsFn(debouncedQuery).then(setSuggestions);
}, [debouncedQuery]);
return {
query,
setQuery,
suggestions,
};
}
Practical Examples
Global Search
Search across the entire site.function GlobalSearch() {
const [query, setQuery] = useState("");
const [results, setResults] = useState<{
users: User[];
posts: Post[];
comments: Comment[];
}>({
users: [],
posts: [],
comments: [],
});
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
// Search multiple entities simultaneously
Promise.all([
userService.search({ query: debouncedQuery }),
postService.search({ query: debouncedQuery }),
commentService.search({ query: debouncedQuery }),
]).then(([users, posts, comments]) => {
setResults({
users: users.users,
posts: posts.posts,
comments: comments.comments,
});
});
}
}, [debouncedQuery]);
return (
<div className="global-search">
<SearchInput
value={query}
onChange={setQuery}
placeholder="Search everything..."
/>
{query && (
<div className="results">
{results.users.length > 0 && (
<div className="result-section">
<h3>Users ({results.users.length})</h3>
{results.users.map((user) => (
<div key={user.id}>{user.username}</div>
))}
</div>
)}
{results.posts.length > 0 && (
<div className="result-section">
<h3>Posts ({results.posts.length})</h3>
{results.posts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)}
{results.comments.length > 0 && (
<div className="result-section">
<h3>Comments ({results.comments.length})</h3>
{results.comments.map((comment) => (
<div key={comment.id}>{comment.content}</div>
))}
</div>
)}
</div>
)}
</div>
);
}
Navigation Search
Search integrated in header.function Header() {
const [isSearchOpen, setIsSearchOpen] = useState(false);
const { query, setQuery, results, isLoading } = useSearch(
(q) => searchService.globalSearch({ query: q })
);
return (
<header>
<div className="logo">My App</div>
<div className="search-container">
{/* Search icon */}
<button onClick={() => setIsSearchOpen(!isSearchOpen)}>
🔍
</button>
{/* Search modal */}
{isSearchOpen && (
<div className="search-modal">
<SearchInput
value={query}
onChange={setQuery}
placeholder="Search..."
autoFocus
/>
{isLoading && <div>Loading...</div>}
{results.length > 0 && (
<div className="quick-results">
{results.slice(0, 5).map((result) => (
<a
key={result.id}
href={`/${result.type}/${result.id}`}
onClick={() => setIsSearchOpen(false)}
>
{result.title}
</a>
))}
</div>
)}
</div>
)}
</div>
<nav>{/* Navigation menu */}</nav>
</header>
);
}
Performance Optimization
Debounce Optimization
Adjust debounce time according to search frequency.// Fast typing: Short debounce
const fastDebounce = useDebounce(query, 200);
// Slow typing: Long debounce
const slowDebounce = useDebounce(query, 500);
// Slow API response: Even longer debounce
const longDebounce = useDebounce(query, 1000);
Caching Strategy
Cache search results with TanStack Query.import { useQuery } from "@tanstack/react-query";
function useSearchWithCache(query: string) {
const debouncedQuery = useDebounce(query, 300);
const { data, error, isLoading } = useQuery({
queryKey: ["search", debouncedQuery],
queryFn: () => searchService.search({ query: debouncedQuery }),
enabled: !!debouncedQuery,
staleTime: 5000, // Stay fresh for 5 seconds
refetchOnWindowFocus: false, // Don't revalidate on focus
});
return {
results: data?.results || [],
isLoading,
error,
};
}
Request Cancellation
Cancel ongoing requests when component unmounts.useEffect(() => {
const abortController = new AbortController();
if (debouncedQuery) {
searchService
.search({ query: debouncedQuery }, { signal: abortController.signal })
.then(setResults);
}
return () => {
abortController.abort();
};
}, [debouncedQuery]);
Cautions
Cautions when using Search Input: 1. Debounce required: 300~500ms recommended 2. Set
minimum search length (usually 2-3 characters) 3. Handle empty string: Prevent returning all
results 4. Support keyboard navigation (accessibility) 5. Search API requires index
optimization 6. Consider pagination for many results
Next Steps
ID Async Select
Relational data selection
Custom Components
Component customization
View Scaffolding
Auto view generation
TanStack Query Hook
Data caching