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

React 18 Hooks Best Practices

Choyeon
Choyeon
Aug 22, 2026 1 min read 683 Views
426 words 2,289 chars Updated Nov 8, 2026
React 18 Hooks Best Practices
Table of contents

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

About the author

Poll

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

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

Comments (5)

0 / 3000
开源贡献者小Z
开源贡献者小Zoss-z.devSep 26, 2026

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

运维老司机
运维老司机ops-notes.cnSep 25, 2026

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

安全小白白
安全小白白seclab.digitalSep 25, 2026

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

算法小菜鸟
算法小菜鸟Sep 24, 2026

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

咖啡与代码
咖啡与代码Sep 24, 2026

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

Related posts

Vue 3 Composition API in Practice

Vue 3 Composition API in Practice

Vue 3's Composition API fundamentally changed how Vue components are organized. Through building a complex e-commerce filter component, this article demonstrates ref vs reactive choices, proper computed/watch usage, custom Composable extraction principles, lifecycle hook timing, and the correct approach to cross-layer state via provide/inject.

1 min read
Tailwind CSS v4: Zero-config + CSS-native Variables

Tailwind CSS v4: Zero-config + CSS-native Variables

Tailwind CSS v4 is an architectural upgrade: goodbye tailwind.config.js, declare design tokens in @theme blocks inside native CSS files. Single @import "tailwindcss" entry replaces postcss.config; built-in CSS vars, native container queries, oxc engine 10-40x faster, simplified color functions.

1 min read
Astro Deep Dive: Islands Architecture & Content Collections

Astro Deep Dive: Islands Architecture & Content Collections

Astro innovations: Islands Architecture for minimal JS with partial hydration, Content Collections for type-safe Markdown/MDX management, ultra-fast zero-JS static sites.

1 min read
Complete Guide to CSS Container Queries

Complete Guide to CSS Container Queries

Modern responsive CSS pillars: Container Queries for component-level responsiveness based on parent size (not viewport), plus :has() parent selector for ancestor styling based on descendants.

1 min read
PWA Offline App in Practice: Service Worker + IndexedDB + Background Sync

PWA Offline App in Practice: Service Worker + IndexedDB + Background Sync

Build installable, offline-first Progressive Web Apps: Service Worker caching strategies (Cache First, Stale-While-Revalidate), IndexedDB wrapper for structured local data, Background Sync queue that replays offline operations when network returns. Full TypeScript code included.

1 min read
Frontend Performance: Core Web Vitals in Practice

Frontend Performance: Core Web Vitals in Practice

Practical Google Core Web Vitals playbook: LCP via preconnects/image-priority/CDN, CLS elimination via image dimensions/font-loading/placeholders, INP via long-task splitting + debouncing.

1 min read