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

import { useState, useEffect } from 'react'

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:

MethodApproachApplicable scenario
Event handler functionFilter directly in onChangeSimple logic, triggered in only one place
useEffect responseuseEffect depends on keywordMultiple 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

import { useState, useEffect } from 'react'

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

// File path: 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('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 box styles (append to 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 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