Advanced useEffect — Real-time Search
In this chapter, you will dive into useEffect's dependency change mechanism to implement real-time filtering and debounce optimization for the search box.
useEffect Dependency Change Trigger
When a value in the useEffect dependency array changes, React will re-run the effect.
Example
function SearchDemo() {
const [keyword, setKeyword] = useState('')
const [results, setResults] = useState([])
// Whenever keyword changes, re-run the filtering
useEffect(() => {
console.log('keyword changed to:', keyword)
// You can execute search logic here
setResults(/* Filtered results */)
}, [keyword]) // Depends on keyword
return (
<div>
<input
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="Search..."
/>
<p>Current search:{keyword}</p>
</div>
)
}
This pattern is suitable for watching a state and automatically executing the corresponding operation when it changes.
Handling in Events vs useEffect
There are two ways to implement search filtering:
| Method | Approach | Applicable scenario |
|---|---|---|
| Event handler function | Filter directly in onChange | Simple logic, triggered in only one place |
| useEffect response | useEffect depends on keyword | Multiple data sources can affect the filter result |
In the blog scenario, the filtering logic for search box input is very simple,It is more appropriate to put it directly in an event handler or useMemo.。
useEffect is more suitable for scenarios that need to sync with external systems (such as calling APIs, saving to localStorage).
Debounce Optimization
If every keystroke triggers a search (local filtering is fine, but if it involves sending network requests), it would cause a lot of waste.
DebounceThe idea: after the user stops typing for a period of time, then execute the operation.
Example
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
// Set a timer: update debouncedValue after delay ms
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
// Cleanup function: if value changes again within delay, clear the previous timer
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
// Use debounce
function SearchInput() {
const [keyword, setKeyword] = useState('')
const debouncedKeyword = useDebounce(keyword, 300)
// Only execute when the debounced value changes (e.g., send a request)
useEffect(() => {
if (debouncedKeyword) {
console.log('Actually executing search:', debouncedKeyword)
// fetch(`/api/search?q=${debouncedKeyword}`)
}
}, [debouncedKeyword])
return (
<input
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="Search..."
/>
)
}
The principle of debounce: each input resets a 300ms timer. Only when there is no new input within 300ms will the timer execute and debouncedKeyword update. This way, the search won't be triggered frequently when the user types quickly.
Hands-on: Add Real-time Search to the Blog
Example
import { useState, useEffect, useMemo } from 'react'
import BlogCard from '../components/BlogCard'
import CategoryFilter from '../components/CategoryFilter'
function HomePage() {
const [articles, setArticles] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
const [activeCategory, setActiveCategory] = useState('All')
const [keyword, setKeyword] = useState('') // Search keyword
// Load data
useEffect(() => {
let cancelled = false
async function fetchPosts() {
setIsLoading(true)
try {
const res = await fetch('/posts.json')
const data = await res.json()
if (!cancelled) setArticles(data)
} catch (err) {
if (!cancelled) setError(err.message)
} finally {
if (!cancelled) setIsLoading(false)
}
}
fetchPosts()
return () => { cancelled = true }
}, [])
const categories = useMemo(() => {
return ['All', ...new Set(articles.map(a => a.category))]
}, [articles])
// Filter + search: filter by both category and keyword
const filteredArticles = useMemo(() => {
let result = articles
// First filter by category
if (activeCategory !== 'All') {
result = result.filter(a => a.category === activeCategory)
}
// Then filter by keyword
if (keyword.trim()) {
const kw = keyword.trim().toLowerCase()
result = result.filter(a =>
a.title.toLowerCase().includes(kw) ||
a.summary.toLowerCase().includes(kw)
)
}
return result
}, [articles, activeCategory, keyword])
return (
<div>
<h2 className="section-title">Latest Articles</h2>
{/* Search box */}
<div className="search-bar">
<input
type="text"
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="Search article titles or summaries..."
className="search-input"
/>
{keyword && (
<span className="clear-btn" onClick={() => setKeyword('')}>✕</span>
)}
</div>
{isLoading && <p className="status-msg">Loading...</p>}
{error && (
<div className="status-msg error">
<p>Failed to load:{error}</p>
<button onClick={() => window.location.reload()}>Retry</button>
</div>
)}
{!isLoading && !error && (
<>
<CategoryFilter
categories={categories}
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
/>
<p className="result-info">
common{filteredArticles.length}article
{keyword &&`, searching for "${keyword}」`}
</p>
{filteredArticles.length === 0 ? (
<p className="empty-tip">No matching articles found</p>
) : (
<div className="article-grid">
{filteredArticles.map(article => (
<BlogCard key={article.id} {...article} />
))}
</div>
)}
</>
)}
</div>
)
}
export default HomePage
Example
.search-bar {
position: relative;
margin-bottom: 20px;
}
.search-input {
width: 100%;
padding: 12px 40px 12px 16px;
border: 2px solid #eee;
border-radius: 8px;
font-size: 15px;
outline: none;
transition: border-color 0.2s;
}
.search-input:focus {
border-color: #1976d2;
}
.clear-btn {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: #999;
font-size: 18px;
}
.status-msg {
text-align: center;
padding: 60px 0;
font-size: 16px;
color: #999;
}
.status-msg.error {
color: #e74c3c;
}
.status-msg.error button {
margin-left: 10px;
padding: 4px 12px;
cursor: pointer;
}
Search and category filtering can work simultaneously — after entering a keyword, clicking a category button applies both conditions to filter.
Chapter Summary
In this chapter, you delved into useEffect's dependency change mechanism: the effect re-executes when values in the dependency array change, the principle and implementation of debounce (setTimeout + cleanup), and the selection boundary between useEffect and event handlers.
The real-time filtering experience of the search box is smooth — this is the value of declarative programming: you declare the computation rule for filteredArticles, and React automatically recalculates it when its dependencies change.
Other Extensions