提示:本网站现在为维护调试状态

React 18 Hooks ベストプラクティス

Choyeon
Choyeon
2026年8月22日 1 分で読了 682 閲覧
426 字 2,289 文字 更新日 2026年11月8日
React 18 Hooks ベストプラクティス
目次

React 18 Hooks Best Practices

React 18's concurrent features have reshaped our understanding of the "rendering process." Traditional Hook usage patterns may expose new issues in concurrent mode. Mastering these best practices is required to truly enjoy React 18's performance dividends.

useEffect Is Synchronous, but the Outside World Isn't

In concurrent mode, an Effect's async callback may arrive long after its cleanup function has already run. The code example uses the standard pattern of 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 and useCallback Optimization Boundaries

Overusing useMemo/useCallback is a hallmark of beginners. Use this decision table to know when to apply them:

Scenario Should Use? Reason
Child wrapped in React.memo receiving reference-type props ✅ Yes Avoids unnecessary re-renders
Used as useEffect dependency and is array/object ✅ Yes Prevents accidental Effect triggers
Expensive computation (sort/iterate >1000 items/heavy regex) ✅ Yes CPU savings outweigh memo overhead
Simple calc like a + b or object with <3 fields ❌ No Memo comparison itself is costlier
Functions passed to native DOM elements ❌ No React doesn't memo DOM callbacks
Functions passed to custom Hooks that don't depend on them ❌ No No tangible benefit

Three Weapons of Concurrent Mode

React 18 provides three keys to unlock concurrent UX:

// 1. startTransition: Mark non-urgent updates
startTransition(() => setFilteredItems(heavyCompute()))

// 2. useDeferredValue: Defer downstream computation of a value
const deferredList = useDeferredValue(hugeList)

// 3. Suspense + use: Await async resources (React 18.2+)
const user = use(fetchUserPromise)

Key difference: useTransition proactively wraps your update action; useDeferredValue passively delays propagation of a specific prop/state.

Three Techniques to Avoid Stale Closures

  1. Ref for latest value: Sync values with useRef + useEffect for async callbacks to read
  2. Functional updates: Pass (prev) => next to setState instead of depending on external values
  3. Treat effects as subscriptions: Fill the dependency array strictly; use eslint-plugin-react-hooks if you don't trust yourself

Best Practices

  1. Always write Effect dependencies honestly — if you want "run once" semantics, first question your design
  2. One Hook does one thing — split custom Hooks over 100 lines
  3. Use mature data-fetching libs (SWR / React Query / TanStack Query) instead of hand-writing useEffect
  4. Lift state to the right level — solve with Lifting State before reaching for Context/Redux
  5. Split Context into fine-grained pieces — a single global UserContext causes mindless re-renders of all consumers

Summary

React 18 is not an incremental upgrade "adding a few Hooks" — it's a ground-up rewrite of the scheduling model. The key to embracing concurrent mode is abandoning the old mindset of "Effect = componentDidMount" and adopting the new paradigm of "declaring synchronization and cleanup relationships of side effects." The purer and more declarative your code, the more React 18 can optimize performance for you.

X微博Telegram

筆者について

投票

你最希望 Rosetta 优先加入哪项功能?

社区投票(演示数据,可后台删除)

コメント (5)

0 / 3000
开源贡献者小Z
开源贡献者小Zoss-z.dev2026年9月26日

有个小小的建议:代码示例如果能加上语法高亮和行号就更好啦,阅读体验会更上一层楼~

运维老司机
运维老司机ops-notes.cn2026年9月25日

感谢分享,刚好需要这个!

安全小白白
安全小白白seclab.digital2026年9月25日

这个组件封装得太优雅了,拿走直接用在项目里了,省了我半天时间,给博主点个大大的赞!

算法小菜鸟
算法小菜鸟2026年9月24日

按你的代码跑了一下,为什么在 Safari 下过渡动画会卡帧?Chrome 和 Firefox 都正常。

咖啡与代码
咖啡与代码2026年9月24日

终于有人把 CSS Grid 的各种布局技巧讲明白了,我之前看 MDN 文档看半天没搞懂,看完这篇直接上手了!

関連記事

Vue 3 コンポジション API 実践

Vue 3 コンポジション API 実践

Vue 3 のコンポジション API は、Vue コンポーネントの設計手法を根本的に変えました。複雑な EC サイトのフィルターコンポーネントの構築を通じて、ref/reactive の使い分け、computed/watch の適切な使用方法、カスタム Composable の抽出原則、ライフサイクルフックのタイミング、provide/inject による階層を超えた状態受け渡しの正しい方法を解説します。

1 分で読了
Tailwind CSS v4 新機能:Zero-config + CSS ネイティブ変数

Tailwind CSS v4 新機能:Zero-config + CSS ネイティブ変数

Tailwind CSS v4はアーキテクチャ級アップデート:tailwind.config.js を廃止し、ネイティブCSS内の@themeブロックでデザイントークン宣言。@import "tailwindcss" 単一エントリでpostcss設定不要。CSS変数内蔵、Container Queriesネイティブ対応、oxcエンジンで10-40倍高速化、カラー関数簡略化。

1 分で読了
Astro 静的サイト:アイランドアーキテクチャ & コンテンツコレクション

Astro 静的サイト:アイランドアーキテクチャ & コンテンツコレクション

Astroの革新:最小JSのアイランドアーキテクチャ、型安全なMarkdown/MDXのコンテンツコレクション、ゼロJSの高速静的サイト構築。

1 分で読了
CSS Container Queries 完全ガイド

CSS Container Queries 完全ガイド

モダンCSSの2大レスポンシブ機能:親コンテナ寸法に応じるContainer Queries、子孫要素の状態で親をスタイルする:has()セレクタ。

1 分で読了
PWA オフラインアプリ実践:Service Worker + IndexedDB + Background Sync

PWA オフラインアプリ実践:Service Worker + IndexedDB + Background Sync

インストール可能・オフラインファーストなPWAを段階的構築:Service Worker キャッシュ戦略(Cache First / Stale-While-Revalidate)、構造化ローカルデータ用IndexedDBラッパー、ネットワーク復旧時にオフライン操作を自動リプレイするBackground Syncキュー。完全TypeScriptコード付き。

1 分で読了
フロントエンド性能:Core Web Vitals 実践

フロントエンド性能:Core Web Vitals 実践

Google検索順位のCore Web Vitals完全実践:LCP最適化(事前接続・画像優先度・CDN)、CLS除去(画像寸法・フォント・プレースホルダ)、INP改善(長タスク分割・デバウンス)。

1 分で読了