useEffect 进阶 — 实时搜索
本章你将深入 useEffect 的依赖变化机制,实现搜索框实时过滤和防抖优化。
useEffect 依赖变化触发
当 useEffect 的依赖数组中某个值变化时,React 会重新执行 effect。
实例
import { useState, useEffect } from 'react'
function SearchDemo() {
const [keyword, setKeyword] = useState('')
const [results, setResults] = useState([])
// 每当 keyword 变化时,重新执行过滤
useEffect(() => {
console.log('keyword 变为:', keyword)
// 这里可以执行搜索逻辑
setResults(/* 过滤结果 */)
}, [keyword]) // 依赖 keyword
return (
<div>
<input
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="搜索..."
/>
<p>当前搜索:{keyword}</p>
</div>
)
}
function SearchDemo() {
const [keyword, setKeyword] = useState('')
const [results, setResults] = useState([])
// 每当 keyword 变化时,重新执行过滤
useEffect(() => {
console.log('keyword 变为:', keyword)
// 这里可以执行搜索逻辑
setResults(/* 过滤结果 */)
}, [keyword]) // 依赖 keyword
return (
<div>
<input
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="搜索..."
/>
<p>当前搜索:{keyword}</p>
</div>
)
}
这种模式适合监听某个 state,在其变化时自动执行相应操作。
在事件中处理 vs useEffect
搜索过滤有两种实现方式:
| 方式 | 写法 | 适用场景 |
|---|---|---|
| 事件处理函数 | onChange 中直接过滤 | 逻辑简单,只在一个地方触发 |
| useEffect 响应 | useEffect 依赖 keyword | 多个数据源都可能影响过滤结果 |
在博客场景中,搜索框输入的过滤逻辑很简单,直接放在事件处理函数或 useMemo 中更合适。
useEffect 更适合那些需要和外部系统同步的场景(如调 API、存 localStorage)。
防抖优化
如果每次按键都触发搜索(本地过滤还好,但如果是发送网络请求),会造成大量浪费。
防抖(Debounce) 的思路:用户停止输入一段时间后,再执行操作。
实例
import { useState, useEffect } from 'react'
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
// 设置定时器:delay ms 后更新 debouncedValue
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
// 清理函数:如果 value 在 delay 内又变了,清除上一个定时器
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
// 使用防抖
function SearchInput() {
const [keyword, setKeyword] = useState('')
const debouncedKeyword = useDebounce(keyword, 300)
// 只在防抖后的值变化时才执行(如发请求)
useEffect(() => {
if (debouncedKeyword) {
console.log('真正执行搜索:', debouncedKeyword)
// fetch(`/api/search?q=${debouncedKeyword}`)
}
}, [debouncedKeyword])
return (
<input
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="搜索..."
/>
)
}
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
// 设置定时器:delay ms 后更新 debouncedValue
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
// 清理函数:如果 value 在 delay 内又变了,清除上一个定时器
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
// 使用防抖
function SearchInput() {
const [keyword, setKeyword] = useState('')
const debouncedKeyword = useDebounce(keyword, 300)
// 只在防抖后的值变化时才执行(如发请求)
useEffect(() => {
if (debouncedKeyword) {
console.log('真正执行搜索:', debouncedKeyword)
// fetch(`/api/search?q=${debouncedKeyword}`)
}
}, [debouncedKeyword])
return (
<input
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="搜索..."
/>
)
}
防抖的原理:每次输入都重置一个 300ms 的定时器,只有当 300ms 内没有新输入时,定时器才会执行,debouncedKeyword 才会更新。这样用户快速打字时不会频繁触发搜索。
动手:给博客加上实时搜索
实例
// 文件路径:src/pages/HomePage.jsx
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('全部')
const [keyword, setKeyword] = useState('') // 搜索关键词
// 加载数据
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 ['全部', ...new Set(articles.map(a => a.category))]
}, [articles])
// 筛选 + 搜索:同时按分类和关键词过滤
const filteredArticles = useMemo(() => {
let result = articles
// 先按分类过滤
if (activeCategory !== '全部') {
result = result.filter(a => a.category === activeCategory)
}
// 再按关键词过滤
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">最新文章</h2>
{/* 搜索框 */}
<div className="search-bar">
<input
type="text"
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="搜索文章标题或摘要..."
className="search-input"
/>
{keyword && (
<span className="clear-btn" onClick={() => setKeyword('')}>✕</span>
)}
</div>
{isLoading && <p className="status-msg">加载中...</p>}
{error && (
<div className="status-msg error">
<p>加载失败:{error}</p>
<button onClick={() => window.location.reload()}>重试</button>
</div>
)}
{!isLoading && !error && (
<>
<CategoryFilter
categories={categories}
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
/>
<p className="result-info">
共 {filteredArticles.length} 篇
{keyword && `,搜索「${keyword}」`}
</p>
{filteredArticles.length === 0 ? (
<p className="empty-tip">没有找到匹配的文章</p>
) : (
<div className="article-grid">
{filteredArticles.map(article => (
<BlogCard key={article.id} {...article} />
))}
</div>
)}
</>
)}
</div>
)
}
export default HomePage
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('全部')
const [keyword, setKeyword] = useState('') // 搜索关键词
// 加载数据
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 ['全部', ...new Set(articles.map(a => a.category))]
}, [articles])
// 筛选 + 搜索:同时按分类和关键词过滤
const filteredArticles = useMemo(() => {
let result = articles
// 先按分类过滤
if (activeCategory !== '全部') {
result = result.filter(a => a.category === activeCategory)
}
// 再按关键词过滤
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">最新文章</h2>
{/* 搜索框 */}
<div className="search-bar">
<input
type="text"
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="搜索文章标题或摘要..."
className="search-input"
/>
{keyword && (
<span className="clear-btn" onClick={() => setKeyword('')}>✕</span>
)}
</div>
{isLoading && <p className="status-msg">加载中...</p>}
{error && (
<div className="status-msg error">
<p>加载失败:{error}</p>
<button onClick={() => window.location.reload()}>重试</button>
</div>
)}
{!isLoading && !error && (
<>
<CategoryFilter
categories={categories}
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
/>
<p className="result-info">
共 {filteredArticles.length} 篇
{keyword && `,搜索「${keyword}」`}
</p>
{filteredArticles.length === 0 ? (
<p className="empty-tip">没有找到匹配的文章</p>
) : (
<div className="article-grid">
{filteredArticles.map(article => (
<BlogCard key={article.id} {...article} />
))}
</div>
)}
</>
)}
</div>
)
}
export default HomePage
实例
/* 搜索框样式(追加到 App.css) */
.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-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;
}
搜索和分类筛选可以同时工作——输入关键词后,再点击分类按钮,两个条件叠加过滤。
本章小结
本章你深入了 useEffect 的依赖变化机制:依赖数组中的值变化时 effect 重新执行、防抖的原理与实现(setTimeout + cleanup)、以及 useEffect vs 事件处理函数的选用边界。
搜索框的实时过滤体验流畅——这就是声明式编程的价值:你声明了 filteredArticles 的计算规则,React 自动在其依赖变化时重新计算。
