React 18 Hooks 最佳实践

Choyeon· 2026年8月22日· 2 分钟阅读· 535 阅读· 704 字· 1,060 字符
React 18 Hooks 最佳实践

React 18 的并发特性改变了我们对"渲染过程"的认知。传统 Hooks 的使用方式在并发模式下可能暴露新的问题。掌握这些最佳实践,才能真正享受到 React 18 的性能红利。

useEffect 是同步的,但外部世界不是

并发模式下,一个 Effect 可能在其清理函数执行后,异步回调才姗姗来迟。代码示例中使用 AbortController + mountedRef 的组合就是标准解法:

import { useState, useEffect, useTransition, useDeferredValue, useMemo, useCallback, useRef } from 'react'

interface User {
  id: number
  name: string
  email: string
  bio: string
}

function useDebounce<T>(value: T, delay: number = 300): T {
  const [debounced, setDebounced] = useState(value)
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay)
    return () => clearTimeout(timer)
  }, [value, delay])
  return debounced
}

function UserSearch() {
  const [query, setQuery] = useState('')
  const [users, setUsers] = useState<User[]>([])
  const [isPending, startTransition] = useTransition()
  const [error, setError] = useState<string | null>(null)
  const abortRef = useRef<AbortController | null>(null)
  const mountedRef = useRef(true)

  const debouncedQuery = useDebounce(query, 250)
  const deferredQuery = useDeferredValue(debouncedQuery)

  useEffect(() => {
    mountedRef.current = true
    return () => {
      mountedRef.current = false
      abortRef.current?.abort()
    }
  }, [])

  useEffect(() => {
    if (!deferredQuery.trim()) {
      setUsers([])
      return
    }
    const controller = new AbortController()
    abortRef.current?.abort()
    abortRef.current = controller
    ;(async () => {
      try {
        setError(null)
        const res = await fetch(`/api/users?q=${encodeURIComponent(deferredQuery)}`, {
          signal: controller.signal
        })
        if (!res.ok) throw new Error(`HTTP ${res.status}`)
        const data = await res.json()
        if (mountedRef.current) {
          startTransition(() => setUsers(data))
        }
      } catch (e: any) {
        if (e.name !== 'AbortError' && mountedRef.current) {
          setError(e.message)
        }
      }
    })()
    return () => controller.abort()
  }, [deferredQuery])

  const highlightText = useCallback((text: string, keyword: string) => {
    if (!keyword) return text
    const parts = text.split(new RegExp(`(${keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'))
    return parts.map((p, i) =>
      p.toLowerCase() === keyword.toLowerCase() ? <mark key={i}>{p}</mark> : <span key={i}>{p}</span>
    )
  }, [])

  const filteredList = useMemo(() => users.slice(0, 50), [users])

  return (
    <div className="search-box">
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="搜索用户..."
        className="input"
      />
      {error && <div className="error">{error}</div>}
      <div className={isPending ? 'results pending' : 'results'}>
        {filteredList.map(user => (
          <div key={user.id} className="user-item">
            <strong>{highlightText(user.name, deferredQuery)}</strong>
            <span className="email">{user.email}</span>
          </div>
        ))}
        {users.length === 0 && deferredQuery && !isPending && !error && (
          <div className="empty">无匹配结果</div>
        )}
      </div>
    </div>
  )
}

useMemo 与 useCallback 优化边界

过度使用 useMemo/useCallback 是初学者的典型特征。下面的决策表帮你判断什么时候该用:

场景 是否应该使用? 理由
子组件被 React.memo 包裹且 props 是引用类型 ✅ 需要 避免不必要重渲染
给 useEffect 作依赖且是数组/对象 ✅ 需要 防止 Effect 意外触发
计算量大(排序/遍历 >1000 项/正则 heavy) ✅ 需要 纯 CPU 性能收益远大于 memo 开销
简单计算如 a + b 或对象只有 3 个字段 ❌ 不要 memo 比较本身更贵
传递给原生 DOM 元素的函数 ❌ 不要 React 不 memo DOM 回调
传递给自定义 Hook 的函数但 Hook 不依赖它 ❌ 不要 无实质好处

并发模式的三大武器

React 18 提供了三把钥匙来解锁并发体验:

// 1. startTransition:标记非紧急更新
startTransition(() => setFilteredItems(heavyCompute()))

// 2. useDeferredValue:延迟某个值的下游计算
const deferredList = useDeferredValue(hugeList)

// 3. Suspense + use:等待异步资源(React 18.2+)
const user = use(fetchUserPromise)

关键区别:useTransition 由你主动包裹更新动作;useDeferredValue 是被动延迟某个 prop/state 的传播。

避免陈旧闭包的三个技巧

  1. Ref 保存最新值:用 useRef + useEffect 同步值给异步回调读取
  2. 函数式更新:setState 传 (prev) => next 而非直接依赖外部值
  3. 把 effect 当"订阅"看:严格填依赖数组,信不过的工具就用 eslint-plugin-react-hooks

最佳实践

  1. Effect 的依赖永远诚实写,实在想模拟"只执行一次"的语义,先反思是不是设计问题
  2. 一个 Hook 只做一件事,自定义 Hook 超过 100 行就考虑拆分
  3. 数据获取用成熟库(SWR / React Query / TanStack Query)而不是手写 useEffect
  4. 状态提升到合适的层级:能用 Lifting State 解决就不要急着上 Context/Redux
  5. Context 拆分细粒度:一个全局 UserContext 会让所有消费组件无脑重渲染

总结

React 18 不是"加了几个 Hook"的增量升级,而是底层调度模型的重构。拥抱并发模式的关键,在于放弃"Effect 是 componentDidMount"的旧思维,改用"声明副作用的同步与清理关系"的新范式。你的代码越纯、越声明式,React 18 就能帮你做得越快。

本文作者

评论 (0)

暂无评论,来抢沙发吧。