现在位置: 首页 > React 教程 > 正文

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>
  )
}

这种模式适合监听某个 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="搜索..."
    />
  )
}

防抖的原理:每次输入都重置一个 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

实例

/* 搜索框样式(追加到 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;
}

搜索和分类筛选可以同时工作——输入关键词后,再点击分类按钮,两个条件叠加过滤。


本章小结

本章你深入了 useEffect 的依赖变化机制:依赖数组中的值变化时 effect 重新执行、防抖的原理与实现(setTimeout + cleanup)、以及 useEffect vs 事件处理函数的选用边界。

搜索框的实时过滤体验流畅——这就是声明式编程的价值:你声明了 filteredArticles 的计算规则,React 自动在其依赖变化时重新计算。