React 的常用 Hooks (including React 18 后的 Hooks)

React Hooks 自 16.8 版本引入以来,彻底改变了函数组件的能力,使其能够管理状态、处理副作用和访问 React 特性。本文将详细介绍 React 中常用的 Hooks,包括基础必备 Hooks、性能优化 Hooks、上下文相关 Hooks,以及 React 18 之后推出的新特性 Hooks,帮助开发者掌握它们的使用场景和实现方式。

useState:函数组件的状态管理基础

作用

useState 是 React 中最基础的 Hook,用于在函数组件中添加状态(state)。它允许组件在渲染之间保存数据,并在数据变化时触发重新渲染。

使用场景

任何需要在组件中维护内部状态的场景,例如:表单输入值、开关状态(如模态框显示/隐藏)、计数器数值等简单状态管理。

代码示例

javascript 复制代码
import { useState } from 'react'
function Counter() {
  // 声明一个状态变量 count,初始值为 0,setCount 是更新 count 的函数
  const [count, setCount] = useState(0)
  return (
    <div>
      <p>当前计数: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        增加
      </button>
      <button onClick={() => setCount(0)}>
        重置
      </button>
    </div>
  )
}
export default Counter

代码解释:useState 接收初始状态作为参数(这里是 0),返回一个数组,第一个元素是当前状态值(count),第二个元素是更新状态的函数(setCount)。调用 setCount 时,React 会重新渲染组件,并使用新的 count 值。

useEffect:副作用处理与生命周期模拟

作用

useEffect 用于处理组件中的"副作用"操作,例如数据获取、订阅事件、DOM 操作等。它可以模拟类组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 三个生命周期方法。

使用场景

  1. 组件挂载后执行一次性操作(如初始化数据获取)
  2. 依赖项变化时执行操作(如根据 props 变化重新获取数据)
  3. 清理副作用(如取消订阅、清除定时器)

代码示例

基础用法:模拟 componentDidMount(仅挂载时执行)

javascript 复制代码
import { useState, useEffect } from 'react'
function UserProfile({ userId }) {
  const [user, setUser] = useState(null)
  // 仅在组件挂载时执行一次(依赖数组为空)
  useEffect(() => {
    console.log('组件挂载完成')
    // 获取用户数据
    fetch(`https://api.example.com/users/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data))
  }, []) // 空依赖数组:仅在组件挂载时执行
  if (!user) return <div>加载中...</div>
  return (
    <div>
      <h1>{user.name}</h1>
      <p>邮箱: {user.email}</p>
    </div>
  )
}

带依赖项:模拟 componentDidUpdate(依赖变化时执行)

javascript 复制代码
// 接上面的 UserProfile 组件,修改 useEffect 依赖项
useEffect(() => {
  console.log(`userId 变化为 ${userId},重新获取数据`)
  fetch(`https://api.example.com/users/${userId}`)
    .then(res => res.json())
    .then(data => setUser(data))
}, [userId]) // 依赖数组包含 userId:userId 变化时执行

清理副作用:模拟 componentWillUnmount

javascript 复制代码
function TimerComponent() {
  const [time, setTime] = useState(0)
  useEffect(() => {
    // 组件挂载时启动定时器
    const timer = setInterval(() => {
      setTime(prevTime => prevTime + 1)
    }, 1000)
    // 返回清理函数:组件卸载或依赖变化前执行
    return () => {
      console.log('清理定时器')
      clearInterval(timer)
    }
  }, []) // 空依赖:仅挂载时启动,卸载时清理
  return <div>已运行: {time} 秒</div>
}

useRef:DOM 引用与持久化值存储

作用

useRef 主要有两个用途:1. 获取 DOM 元素的引用;2. 在组件渲染之间持久化存储一个值(该值变化不会触发组件重新渲染)。

使用场景

  1. 直接操作 DOM 元素(如聚焦输入框、获取元素尺寸)
  2. 存储不需要触发渲染的持久化数据(如定时器 ID、上一次渲染的状态值)

代码示例

用途 1:获取 DOM 元素引用

javascript 复制代码
import { useRef, useEffect } from 'react'
function InputFocus() {
  // 创建一个 ref 对象
  const inputRef = useRef(null)
  useEffect(() => {
    // 组件挂载后,让输入框自动聚焦
    inputRef.current.focus()
  }, [])
  return (
    <input
      ref={inputRef} // 将 ref 绑定到 DOM 元素
      type="text"
      placeholder="自动聚焦的输入框"
    />
  )
}

用途 2:持久化存储值(不触发渲染)

javascript 复制代码
import { useRef, useState } from 'react'
function CounterWithPrev() {
  const [count, setCount] = useState(0)
  // 存储上一次的 count 值
  const prevCountRef = useRef(null)
  const handleIncrement = () => {
    prevCountRef.current = count // 更新 ref 的 current 值(不会触发渲染)
    setCount(count + 1)
  }
  return (
    <div>
      <p>当前计数: {count}</p>
      <p>上一次计数: {prevCountRef.current ?? '未记录'}</p>
      <button onClick={handleIncrement}>增加</button>
    </div>
  )
}

useReducer:复杂状态逻辑的管理

作用

useReduceruseState 的替代方案,用于管理包含多个子值的复杂状态逻辑,或当状态转换逻辑复杂且需要复用、预测时。它基于 Redux 的思想,通过"动作(action)"来描述状态变化,并使用" reducer 函数"来处理状态转换。

使用场景

  1. 状态逻辑复杂(如包含多个子状态,且状态更新依赖于前一个状态)
  2. 多个组件需要共享状态更新逻辑
  3. 需要预测和测试状态变化(reducer 是纯函数,输入相同则输出相同)

代码示例

计数器示例(基础用法)

javascript 复制代码
import { useReducer } from 'react'
// 定义 reducer 函数:接收当前状态和动作,返回新状态
function countReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 }
    case 'DECREMENT':
      return { ...state, count: state.count - 1 }
    case 'RESET':
      return { ...state, count: 0 }
    default:
      throw new Error(`未知动作类型: ${action.type}`)
  }
}
function CounterWithReducer() {
  // 初始化状态和 dispatch 函数
  const [state, dispatch] = useReducer(countReducer, { count: 0 })
  return (
    <div>
      <p>当前计数: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>增加</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>减少</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
    </div>
  )
}

表单状态管理(复杂状态示例)

javascript 复制代码
import { useReducer } from 'react'
function formReducer(state, action) {
  switch (action.type) {
    case 'UPDATE_FIELD':
      return {
        ...state,
        [action.field]: action.value
      }
    case 'RESET_FORM':
      return action.initialState
    default:
      return state
  }
}
function LoginForm() {
  const initialState = {
    username: '',
    password: ''
  }
  const [formState, dispatch] = useReducer(formReducer, initialState)
  const handleSubmit = (e) => {
    e.preventDefault()
    console.log('提交表单:', formState)
  }
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>用户名:</label>
        <input
          type="text"
          value={formState.username}
          onChange={(e) => dispatch({
            type: 'UPDATE_FIELD',
            field: 'username',
            value: e.target.value
          })}
        />
      </div>
      <div>
        <label>密码:</label>
        <input
          type="password"
          value={formState.password}
          onChange={(e) => dispatch({
            type: 'UPDATE_FIELD',
            field: 'password',
            value: e.target.value
          })}
        />
      </div>
      <button type="submit">登录</button>
      <button
        type="button"
        onClick={() => dispatch({ type: 'RESET_FORM', initialState })}
      >
        重置
      </button>
    </form>
  )
}

useContext:跨组件状态共享

作用

useContext 用于在函数组件中访问 React 的上下文(Context),实现跨层级组件间的数据共享,避免通过 props 逐层传递数据("prop drilling"问题)。

使用场景

  1. 多个组件需要访问同一数据(如用户信息、主题设置、语言偏好)
  2. 组件层级较深,通过 props 传递数据繁琐
  3. 非父子关系组件间的数据共享

代码示例

步骤 1:创建 Context

javascript 复制代码
// ThemeContext.js
import { createContext } from 'react'
// 创建上下文,可提供默认值
const ThemeContext = createContext('light')
export default ThemeContext

步骤 2:在父组件中提供 Context 值

javascript 复制代码
// App.js
import { useState } from 'react'
import ThemeContext from './ThemeContext'
import ThemedButton from './ThemedButton'
function App() {
  const [theme, setTheme] = useState('light')
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light')
  }
  return (
    // 使用 Provider 包裹需要访问 Context 的组件树
    <ThemeContext.Provider value={theme}>
      <div style={{
        padding: '20px',
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#333' : '#fff'
      }}>
        <h1>当前主题: {theme}</h1>
        <button onClick={toggleTheme}>切换主题</button>
        <ThemedButton />
      </div>
    </ThemeContext.Provider>
  )
}

步骤 3:在子组件中使用 useContext 访问 Context

csharp 复制代码
// ThemedButton.js
import { useContext } from 'react'
import ThemeContext from './ThemeContext'
function ThemedButton() {
  // 使用 useContext 获取 ThemeContext 的值
  const theme = useContext(ThemeContext)
  return (
    <button style={{
      padding: '8px 16px',
      backgroundColor: theme === 'light' ? '#007bff' : '#6c757d',
      color: '#fff',
      border: 'none',
      borderRadius: '4px',
      marginTop: '10px'
    }}>
      主题按钮
    </button>
  )
}
export default ThemedButton

useMemo:计算结果的缓存与性能优化

作用

useMemo 用于缓存"昂贵计算"的结果,避免在每次组件渲染时重复执行这些计算,从而优化性能。它接收一个计算函数和依赖数组,只有当依赖项发生变化时,才会重新执行计算函数并更新缓存结果。

使用场景

  1. 执行昂贵的计算操作(如大数据排序、复杂数据转换)
  2. 避免在每次渲染时创建新的对象/数组(导致子组件不必要的重渲染)

代码示例

基础用法:缓存昂贵计算结果

javascript 复制代码
import { useState, useMemo } from 'react'
function ExpensiveCalculation() {
  const [numbers] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
  const [multiplier, setMultiplier] = useState(1)
  // 昂贵的计算:将数组中所有数字乘以 multiplier 并求和
  const calculateTotal = () => {
    console.log('执行计算...') // 用于观察计算是否执行
    return numbers.reduce((sum, num) => sum + num * multiplier, 0)
  }
  // 使用 useMemo 缓存计算结果,仅当 multiplier 变化时重新计算
  const total = useMemo(calculateTotal, [multiplier])
  return (
    <div>
      <p>乘数: {multiplier}</p>
      <button onClick={() => setMultiplier(m => m + 1)}>增加乘数</button>
      <p>计算结果: {total}</p>
    </div>
  )
}

进阶用法:避免创建新对象导致子组件重渲染

javascript 复制代码
import { useState, useMemo } from 'react'
// 子组件:接收 user 对象作为 props
const UserCard = ({ user }) => {
  console.log(`UserCard 渲染: ${user.name}`) // 观察是否重渲染
  return (
    <div>
      <h3>{user.name}</h3>
      <p>年龄: {user.age}</p>
    </div>
  )
}
// 父组件
function UserProfile() {
  const [name, setName] = useState('张三')
  const [age, setAge] = useState(25)
  const [count, setCount] = useState(0)
  // 使用 useMemo:仅当 name 或 age 变化时才创建新对象
  const user = useMemo(() => ({ name, age }), [name, age])
  return (
    <div>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="修改姓名"
      />
      <button onClick={() => setAge(a => a + 1)}>增加年龄</button>
      <p>无关计数: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>增加计数</button>
      <UserCard user={user} />
    </div>
  )
}

解释:当使用 useMemo 后,只有 nameage 变化时,user 对象才会更新,避免了因 count 变化导致的 UserCard 不必要重渲染。

useCallback:函数引用的缓存与性能优化

作用

useCallback 用于缓存函数的引用,避免在每次组件渲染时创建新的函数实例。它通常与 React.memo 配合使用,防止因函数 props 变化导致子组件不必要的重渲染。

使用场景

  1. 将函数作为 props 传递给子组件,且子组件使用 React.memo 优化
  2. 函数作为 useEffect 的依赖项,避免因函数引用变化导致副作用重复执行

代码示例

基础用法:避免子组件因函数 props 变化重渲染

javascript 复制代码
import { useState, useCallback, memo } from 'react'
// 使用 memo 包装子组件,仅当 props 浅变化时才重渲染
const ActionButton = memo(({ onClick, label }) => {
  console.log(`ActionButton "${label}" 渲染`)
  return <button onClick={onClick}>{label}</button>
})
function ParentComponent() {
  const [count, setCount] = useState(0)
  // 使用 useCallback 缓存函数引用,仅当依赖变化时才创建新函数
  const handleIncrement = useCallback(() => {
    setCount(c => c + 1)
  }, []) // 空依赖:函数引用永久不变
  return (
    <div>
      <p>计数: {count}</p>
      <ActionButton onClick={handleIncrement} label="增加" />
      <ActionButton onClick={() => setCount(0)} label="重置" />
    </div>
  )
}

进阶用法:作为 useEffect 依赖项

javascript 复制代码
import { useState, useCallback, useEffect } from 'react'
function DataFetcher({ userId }) {
  const [data, setData] = useState(null)
  // 使用 useCallback 缓存 fetchData 函数
  const fetchData = useCallback(async () => {
    const response = await fetch(`https://api.example.com/users/${userId}`)
    const result = await response.json()
    setData(result)
  }, [userId]) // 仅当 userId 变化时,函数引用才更新
  // 依赖 fetchData 函数,但由于 useCallback 缓存,仅在 userId 变化时执行
  useEffect(() => {
    fetchData()
  }, [fetchData])
  if (!data) return <div>加载中...</div>
  return <div>用户名: {data.name}</div>
}

useLayoutEffect:DOM 更新后的同步操作

作用

useLayoutEffectuseEffect 功能类似,但执行时机不同:它在 DOM 更新后同步执行 (阻塞浏览器绘制),而 useEffect 在 DOM 更新后异步执行(不阻塞绘制)。

使用场景

  1. 需要读取 DOM 布局并立即执行操作(如测量元素尺寸后调整位置)
  2. 避免因异步执行导致的视觉闪烁(如模态框定位计算)

代码示例

javascript 复制代码
import { useRef, useLayoutEffect, useState } from 'react'
function Tooltip() {
  const [position, setPosition] = useState({ top: 0, left: 0 })
  const targetRef = useRef(null)
  const tooltipRef = useRef(null)
  useLayoutEffect(() => {
    if (!targetRef.current || !tooltipRef.current) return
    // 读取 DOM 布局信息(同步执行)
    const targetRect = targetRef.current.getBoundingClientRect()
    const tooltipRect = tooltipRef.current.getBoundingClientRect()
    // 计算 tooltip 位置(避免溢出视口)
    const top = targetRect.bottom + window.scrollY + 5
    const left = targetRect.left + window.scrollX - (tooltipRect.width - targetRect.width) / 2
    setPosition({ top, left })
  }, []) // 组件挂载后计算一次位置
  return (
    <div>
      <button ref={targetRef}>hover 显示提示</button>
      <div
        ref={tooltipRef}
        style={{
          position: 'absolute',
          top: position.top,
          left: position.left,
          background: '#333',
          color: 'white',
          padding: '4px 8px',
          borderRadius: '4px'
        }}
      >
        这是提示内容
      </div>
    </div>
  )
}

React 18 新增 Hooks

useId:唯一 ID 生成器

作用

useId 用于生成跨服务端和客户端的 唯一且稳定的 ID ,解决 SSR(服务端渲染)中的" hydration 不匹配"问题。它生成的 ID 以 : 开头,确保全局唯一性。

使用场景

  1. 为表单元素生成 idhtmlFor 属性(关联 label 和 input)
  2. 为无障碍(a11y)属性生成唯一标识符(如 aria-labelledby
  3. 避免手动生成 ID 导致的 SSR 不匹配问题

代码示例

javascript 复制代码
import { useId } from 'react'
function FormInput() {
  // 生成唯一 ID
  const inputId = useId()
  // 可基于基础 ID 生成关联 ID
  const errorId = `${inputId}-error`
  return (
    <div>
      <label htmlFor={inputId}>用户名:</label>
      <input
        id={inputId}
        type="text"
        aria-describedby={errorId} // 关联错误提示
      />
      <p id={errorId} style={{ color: 'red' }}>
        用户名不能为空
      </p>
    </div>
  )
}

useTransition:非阻塞状态更新

作用

useTransition 允许将某些状态更新标记为"非紧急",优先保证 UI 响应性。React 会优先处理紧急更新(如输入框输入),延迟处理非紧急更新(如大型列表过滤),避免页面卡顿。

使用场景

  1. 大型列表过滤或排序(数据量大时避免阻塞 UI)
  2. 复杂状态计算(不影响用户即时交互的操作)
  3. 表单提交前的预验证(不阻塞用户输入)

代码示例

javascript 复制代码
import { useState, useTransition } from 'react'
function SearchList({ items }) {
  const [query, setQuery] = useState('')
  const [filteredItems, setFilteredItems] = useState(items)
  // isPending: 标记过渡是否进行中;startTransition: 包装非紧急更新
  const [isPending, startTransition] = useTransition()
  const handleChange = (e) => {
    // 紧急更新:立即更新输入框值(用户能感知的交互)
    setQuery(e.target.value)
    // 非紧急更新:标记为过渡,React 会在空闲时执行
    startTransition(() => {
      // 过滤大型列表(可能耗时)
      const result = items.filter(item =>
        item.name.toLowerCase().includes(query.toLowerCase())
      )
      setFilteredItems(result)
    })
  }
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleChange}
        placeholder="搜索..."
      />
      {isPending ? (
        <p>加载中...</p>
      ) : (
        <ul>
          {filteredItems.map(item => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ul>
      )}
    </div>
  )
}

useDeferredValue:延迟更新低优先级值

作用

useDeferredValueuseTransition 类似,用于延迟更新"低优先级"的值,但它直接作用于值而非更新函数。当原值变化时,React 会先使用旧值渲染,待空闲后再更新为新值。

使用场景

  1. 显示大型列表的过滤结果(保持输入框响应性)
  2. 延迟更新非关键 UI 区域(如侧边栏统计数据)

代码示例

javascript 复制代码
import { useState, useDeferredValue } from 'react'
function ProductList({ products }) {
  const [searchTerm, setSearchTerm] = useState('')
  
  // 延迟更新 searchTerm 的值(低优先级)
  const deferredSearchTerm = useDeferredValue(searchTerm)
  // 基于延迟值过滤列表(仅在 deferredSearchTerm 更新时执行)
  const filteredProducts = products.filter(product =>
    product.name.toLowerCase().includes(deferredSearchTerm.toLowerCase())
  )
  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="搜索商品..."
      />
      <div>
        {filteredProducts.map(product => (
          <div key={product.id}>{product.name}</div>
        ))}
      </div>
    </div>
  )
}

useSyncExternalStore:外部数据订阅

作用

useSyncExternalStore 用于订阅外部数据源(如 Redux、 Zustand 等状态管理库,或浏览器 API 如 localStorage),确保在 React 并发渲染模式下数据的一致性和可预测性。

使用场景

  1. 订阅外部状态管理库(替代 useEffect 手动订阅)
  2. 监听浏览器 API 变化(如 localStoragesessionStorage
  3. 确保并发模式下的数据安全访问

代码示例

javascript 复制代码
import { useSyncExternalStore } from 'react'
// 模拟外部数据源(如 Redux store)
const store = {
  state: { count: 0 },
  listeners: [],
  subscribe(listener) {
    this.listeners.push(listener)
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener)
    }
  },
  getState() {
    return this.state
  },
  increment() {
    this.state.count++
    this.listeners.forEach(listener => listener())
  }
}
// 自定义 Hook 封装订阅逻辑
function useStore(selector) {
  return useSyncExternalStore(
    store.subscribe.bind(store), // 订阅函数
    () => selector(store.getState()), // 获取当前状态
    () => selector({ count: 0 }) // 服务端初始状态
  )
}
// 使用外部数据
function Counter() {
  const count = useStore(state => state.count)
  
  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={() => store.increment()}>增加</button>
    </div>
  )
}

useInsertionEffect:CSS-in-JS 样式插入

作用

useInsertionEffect 是 React 18 为 CSS-in-JS 库提供的特殊 Hook,它在 DOM 元素插入前执行 ,用于动态插入样式规则,避免样式闪烁问题。执行时机早于 useLayoutEffect

使用场景

  1. CSS-in-JS 库动态插入样式(如 styled-components、Emotion)
  2. 需要在 DOM 渲染前注入关键样式的场景

代码示例

javascript 复制代码
import { useInsertionEffect, useState } from 'react'
// 简化的 CSS-in-JS 实现
function useCSS(style) {
  const styleRef = useRef(null)
  useInsertionEffect(() => {
    // 创建 style 标签并插入样式(在 DOM 元素插入前执行)
    styleRef.current = document.createElement('style')
    styleRef.current.textContent = style
    document.head.appendChild(styleRef.current)
    return () => {
      document.head.removeChild(styleRef.current)
    }
  }, [style])
  return styleRef
}
function StyledButton() {
  const [color, setColor] = useState('blue')
  // 动态生成样式
  useCSS(`
    .custom-button {
      background: ${color};
      color: white;
      padding: 8px 16px;
      border: none;
      border-radius: 4px;
    }
  `)
  return (
    <div>
      <button 
        className="custom-button"
        onClick={() => setColor('red')}
      >
        点击变色
      </button>
    </div>
  )
}

其他实用 Hooks

useImperativeHandle:自定义暴露实例值

作用

useImperativeHandle 用于自定义通过 ref 暴露给父组件的实例值,避免将子组件的完整 DOM 实例暴露出去,增强组件封装性。

使用场景

  1. 父组件需要调用子组件的特定方法(如表单提交、重置)
  2. 限制父组件可访问的子组件功能(避免直接操作 DOM)

代码示例

javascript 复制代码
import { useRef, useImperativeHandle, forwardRef } from 'react'
// 使用 forwardRef 将 ref 传递给子组件
const CustomInput = forwardRef((props, ref) => {
  const inputRef = useRef(null)
  // 自定义暴露给父组件的方法
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus()
    },
    clear: () => {
      inputRef.current.value = ''
    }
  }))
  return <input ref={inputRef} {...props} />
})
// 父组件使用子组件暴露的方法
function ParentComponent() {
  const inputRef = useRef(null)
  return (
    <div>
      <CustomInput ref={inputRef} placeholder="点击按钮操作我" />
      <button onClick={() => inputRef.current.focus()}>聚焦</button>
      <button onClick={() => inputRef.current.clear()}>清空</button>
    </div>
  )
}

useDebugValue:自定义 Hook 调试信息

作用

useDebugValue 用于在 React DevTools 中显示自定义 Hook 的标签和值,方便调试复杂的自定义 Hook。

使用场景

  1. 开发共享自定义 Hook(如 useLocalStorageuseFetch
  2. 增强自定义 Hook 的调试体验

代码示例

javascript 复制代码
import { useState, useEffect, useDebugValue } from 'react'
// 自定义 Hook:获取窗口尺寸
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  })
  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      })
    }
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])
  // 在 DevTools 中显示调试信息
  useDebugValue(`width: ${size.width}, height: ${size.height}`)
  
  return size
}
// 使用自定义 Hook
function ResponsiveComponent() {
  const { width, height } = useWindowSize()
  return (
    <div>
      <p>窗口尺寸: {width} x {height}</p>
    </div>
  )
}

总结:Hooks 最佳实践与注意事项

核心原则

  1. 只在顶层调用 Hooks:不要在循环、条件或嵌套函数中调用 Hooks,确保每次渲染时 Hooks 调用顺序一致。
  2. 只在函数组件或自定义 Hook 中调用:避免在普通 JavaScript 函数中使用 Hooks。
  3. 依赖数组要完整useEffectuseMemouseCallback 的依赖数组应包含所有外部变量,避免闭包陷阱。

性能优化建议

  1. 避免过度优化useMemouseCallback 本身有性能开销,仅在确有性能问题时使用。
  2. 合理拆分组件:将复杂逻辑拆分为小型组件,减少不必要的重渲染。
  3. 使用 React.memo 谨慎 :仅对纯展示组件使用 React.memo,避免浅比较成本高于重渲染成本。

常见陷阱

  1. 闭包陷阱useEffect 中依赖项未更新时,内部函数可能捕获旧的状态值。
  2. 过度使用 Context:Context 变化会导致所有消费组件重渲染,复杂状态建议使用状态管理库。
  3. 忽略清理函数 :忘记清理 useEffect 中的订阅或定时器,可能导致内存泄漏。

通过合理使用和组合这些 Hooks,开发者可以编写出更简洁、可维护且高性能的 React 应用。React 18 新增的 Hooks 进一步增强了并发渲染下的用户体验,建议在项目中逐步实践和迁移。# React 18 新增 Hooks

useId:唯一 ID 生成器

作用

useId 是 React 18 引入的用于生成唯一 ID 的 Hook,特别适用于需要在服务端渲染 (SSR) 中避免 hydration 不匹配的场景。它生成的 ID 带有稳定的前缀,确保客户端和服务端生成的 ID 一致。

使用场景

  1. 为表单元素生成关联的 idhtmlFor 属性
  2. 为无障碍 (a11y) 属性生成唯一标识符(如 aria-labelledby
  3. 避免 SSR 时因随机 ID 导致的 hydration 警告

代码示例

javascript 复制代码
import { useId } from 'react'
function FormInput() {
  // 生成唯一 ID
  const inputId = useId()
  const passwordId = useId()
  return (
    <div>
      <label htmlFor={inputId}>
        用户名:
        <input id={inputId} type="text" placeholder="请输入用户名" />
      </label>
      
      <label htmlFor={passwordId} style={{ marginLeft: '10px' }}>
        密码:
        <input id={passwordId} type="password" placeholder="请输入密码" />
      </label>
    </div>
  )
}

注意:useId 生成的 ID 包含 : 字符,不适合用于 CSS 选择器或 querySelector。

useTransition:非阻塞状态更新

作用

useTransition 允许将某些状态更新标记为"非紧急",React 会优先处理紧急更新(如输入框输入),延迟处理非紧急更新,从而避免 UI 卡顿,提升用户体验。

使用场景

  1. 大型列表过滤或排序(如搜索框输入过滤长列表)
  2. 复杂数据计算或转换(不希望阻塞用户输入)
  3. 任何可能导致 UI 卡顿的非紧急状态更新

代码示例

javascript 复制代码
import { useState, useTransition } from 'react'
function SearchList({ items }) {
  const [query, setQuery] = useState('')
  const [filteredItems, setFilteredItems] = useState(items)
  // isPending 表示过渡是否进行中,startTransition 包装非紧急更新
  const [isPending, startTransition] = useTransition()
  const handleSearch = (e) => {
    const newQuery = e.target.value
    // 紧急更新:立即更新输入框的值
    setQuery(newQuery)
    
    // 标记为非紧急更新:过滤列表(可能耗时)
    startTransition(() => {
      setFilteredItems(
        items.filter(item => 
          item.name.toLowerCase().includes(newQuery.toLowerCase())
        )
      )
    })
  }
  return (
    <div>
      <input 
        type="text" 
        value={query} 
        onChange={handleSearch} 
        placeholder="搜索..." 
      />
      {isPending ? (
        <p>加载中...</p>
      ) : (
        <ul>
          {filteredItems.map(item => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ul>
      )}
    </div>
  )
}

useDeferredValue:延迟更新低优先级值

作用

useDeferredValueuseTransition 类似,用于延迟更新低优先级的值。不同之处在于,useDeferredValue 是对值进行延迟,而 useTransition 是对更新函数进行延迟包装。

使用场景

  1. 当某个值的计算可能阻塞 UI,但又无法通过 useTransition 包装(如从 props 接收的值)
  2. 需要基于延迟值进行渲染,且希望保持组件结构简洁

代码示例

javascript 复制代码
import { useState, useDeferredValue } from 'react'
function ProductList({ products }) {
  const [searchQuery, setSearchQuery] = useState('')
  // 延迟更新搜索查询(低优先级)
  const deferredQuery = useDeferredValue(searchQuery)
  // 基于延迟值过滤产品(避免每次输入都立即过滤)
  const filteredProducts = products.filter(product => 
    product.name.toLowerCase().includes(deferredQuery.toLowerCase())
  )
  return (
    <div>
      <input
        type="text"
        value={searchQuery}
        onChange={(e) => setSearchQuery(e.target.value)}
        placeholder="搜索产品..."
      />
      <div>
        {filteredProducts.map(product => (
          <div key={product.id}>{product.name}</div>
        ))}
      </div>
    </div>
  )
}

useSyncExternalStore:外部数据订阅

作用

useSyncExternalStore 用于订阅外部数据源(如 Redux、 Zustand 等状态管理库,或浏览器 API 如 localStorage),确保在并发渲染模式下数据的一致性和可预测性。

使用场景

  1. 订阅外部状态管理库(替代 useEffect 手动订阅)
  2. 监听浏览器 API 变化(如 localStoragesessionStorage
  3. 确保并发模式下的数据同步

代码示例

javascript 复制代码
import { useSyncExternalStore } from 'react'
// 模拟外部数据源(如 Redux store)
const store = {
  state: { count: 0 },
  listeners: [],
  subscribe(listener) {
    this.listeners.push(listener)
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener)
    }
  },
  getState() {
    return this.state
  },
  increment() {
    this.state.count++
    this.listeners.forEach(listener => listener())
  }
}
// 自定义 Hook 封装订阅逻辑
function useStore(selector) {
  return useSyncExternalStore(
    store.subscribe.bind(store), // 订阅函数
    () => selector(store.getState()), // 获取当前状态
    () => selector({ count: 0 }) // 服务端初始状态
  )
}
function Counter() {
  const count = useStore(state => state.count)
  
  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={() => store.increment()}>增加</button>
    </div>
  )
}

useInsertionEffect:CSS-in-JS 样式插入

作用

useInsertionEffect 是 React 18 新增的副作用 Hook,其执行时机在 DOM 变更之前,比 useLayoutEffect 更早。主要用于 CSS-in-JS 库在渲染前插入样式,避免样式闪烁(Flash of Unstyled Content, FOUC)。

使用场景

  1. CSS-in-JS 库内部实现样式插入
  2. 需要在 DOM 元素渲染前注入关键样式
  3. 性能敏感的样式操作(避免布局偏移)

代码示例

javascript 复制代码
import { useInsertionEffect, useState } from 'react'
// 模拟 CSS-in-JS 样式插入
function useCSS(styles) {
  useInsertionEffect(() => {
    // 在 DOM 更新前插入样式
    const styleSheet = document.createElement('style')
    styleSheet.textContent = styles
    document.head.appendChild(styleSheet)
    
    return () => {
      document.head.removeChild(styleSheet)
    }
  }, [styles])
}
function StyledComponent() {
  const [color, setColor] = useState('red')
  
  // 使用 useInsertionEffect 注入样式
  useCSS(`
    .styled-div {
      color: ${color};
      font-size: 20px;
      padding: 10px;
    }
  `)
  
  return (
    <div>
      <div className="styled-div">动态样式文本</div>
      <button onClick={() => setColor(color === 'red' ? 'blue' : 'red')}>
        切换颜色
      </button>
    </div>
  )
}

总结:Hooks 最佳实践与注意事项

基础原则

  1. 只在函数组件或自定义 Hook 中调用 Hooks,不要在普通函数、循环或条件语句中调用
  2. 遵循依赖数组规则 :确保 useEffectuseMemouseCallback 的依赖数组完整包含所有外部变量
  3. 避免过度优化useMemouseCallback 有性能开销,仅在确实存在性能问题时使用

常见陷阱

  1. 闭包陷阱useEffect 中访问的状态是捕获的渲染时的状态,如需最新状态可使用 useRef 存储
  2. 依赖缺失:忘记在依赖数组中添加变量,导致副作用逻辑使用旧值
  3. 过度使用 ContextuseContext 会导致消费组件在 Context 值变化时重新渲染,避免存储频繁变化的值

自定义 Hook 组合

通过组合内置 Hooks 可以创建自定义 Hook,封装复用逻辑:

scss 复制代码
// 自定义 Hook:带防抖的输入处理
function useDebouncedInput(initialValue, delay = 300) {
  const [value, setValue] = useState(initialValue)
  const [debouncedValue, setDebouncedValue] = useState(initialValue)
  const timerRef = useRef(null)
  useEffect(() => {
    // 清除上一次定时器
    if (timerRef.current) clearTimeout(timerRef.current)
    // 设置新定时器
    timerRef.current = setTimeout(() => {
      setDebouncedValue(value)
    }, delay)
    
    return () => {
      clearTimeout(timerRef.current)
    }
  }, [value, delay])
  return [value, setValue, debouncedValue]
}

React Hooks 提供了强大的能力来简化组件逻辑和优化性能。掌握本文介绍的常用 Hooks 及其使用场景,能够帮助开发者编写更简洁、高效和可维护的 React 应用。随着 React 版本的迭代,新的 Hooks 会不断出现,建议持续关注官方文档以获取最新信息。

相关推荐
萌萌哒草头将军4 小时前
🚀🚀🚀React Router 现在支持 SRC 了!!!
javascript·react.js·preact
薛定谔的算法5 小时前
# 从0到1构建React项目:一个仓库展示应用的架构实践
前端·react.js
一嘴一个橘子7 小时前
react 路由 react-router-dom
react.js
薛定谔的算法7 小时前
# 前端路由进化史:从白屏到丝滑体验的技术突围
前端·react.js·前端框架
Adolf_19939 小时前
React 中 props 的最常用用法精选+useContext
前端·javascript·react.js
前端小趴菜059 小时前
react - 根据路由生成菜单
前端·javascript·react.js
極光未晚9 小时前
React Hooks 中的时空穿梭:模拟 ComponentDidMount 的奇妙冒险
前端·react.js·源码
孟陬11 小时前
写一个根据屏幕尺寸动态隐藏元素的插件 🧩 - tailwindcss 系列
react.js
啃火龙果的兔子12 小时前
nextjs+react项目如何代理本地请求解决跨域
前端·react.js·前端框架
WildBlue12 小时前
🚀 React Fragment:让代码呼吸的新鲜空气
前端·react.js