How can Next.js simply implement a search feature?
"TLDR: This article provides a detailed explanation of how to implement search functionality in Next.js, covering both frontend debounce design and backend database query strategies. On the frontend, a custom hook `useDebounce` is used to delay triggering search requests based on input, preventing excessive requests. On the backend, Prisma is initially used to perform fuzzy matching queries directly against the database, with a note that this can be extended to Elasticsearch in the future to handle high-concurrency scenarios."
I've been working on a startup project over the past two days, and we've already reached 100 registered users—so exciting!
Most of the project's code was written by AI, with me acting more as a product manager + bug fixer, making sure the AI doesn't lose itself in its own infinite generation. The upside is that we got to launch quickly, but the downside is that I haven't truly learned a lot of the code myself, which could cause problems down the road—like with the search functionality.
Debounce Design
Implementing real-time search on the frontend is relatively easy. By setting up state:
const [query, setQuery] = useState("") // to hold the real-time state of the input field
useEffect(()=> {
performSearch(query); // if query changes, send a search request to the backend
}, [query])
However, if you want to implement debouncing (delaying the request by 500ms), the useEffect trigger condition can no longer be query. Instead, you need to find a way to set up a delayed value:
const [query, setQuery] = useStat("") // holds the real-time search keyword
function useDebounce<T>(value: T, delay?: number): T{
const [debouunceValue, setDebounceValue] = useState<T>(value) // the delayed value
useEffect(()=>{
const timer = setTimeout(() => setDebounceValue(value), delay || 500);
return () => {
clearTimeout(timer)
}
}, [value, delay]); // as long as value changes, debounce changes
return debounceValue;
}
const debouncedQuery = useDebounce(query, 500);
useEffect(()=>{
performSearch(debounceQuery);
}, [debouncedQuery])
By introducing a new state debounceValue that depends on query, it only changes 500ms after query changes.
For example:
- The user types "a" at 0ms, so the timer is set to start searching for "a" at 500ms.
- The user types "b" at 300ms, which will clear the previous timer (clearTimeout(timer)) and create a new one, scheduled to search for "b" at 800ms.
In other words: debounceValue always changes 500ms after query changes.
Backend Search
When the amount of data is very small, you can directly query the database using contains for keyword search:
export const findResourcesByKeyword = cache(
async (keyword: string, limit: number = 10) => {
const searchTerm = `%${keyword}%`;
return prisma.resources.findMany({
where: {
OR: [
{
title: {
contains: keyword,
mode: 'insensitive'
}
},
{
description: {
contains: keyword,
mode: 'insensitive'
}
},
{
subject: {
contains: keyword,
mode: 'insensitive'
}
},
{
category: {
contains: keyword,
mode: 'insensitive'
}
}
]
},
select: resourceSelect,
orderBy: [
{ created_at: 'desc' }
],
take: limit
});
});
If the data volume and concurrency increase in the future, this function will need to be replaced with a professional search middleware like Elasticsearch.