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 操作等。它可以模拟类组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
三个生命周期方法。
使用场景
- 组件挂载后执行一次性操作(如初始化数据获取)
- 依赖项变化时执行操作(如根据 props 变化重新获取数据)
- 清理副作用(如取消订阅、清除定时器)
代码示例
基础用法:模拟 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. 在组件渲染之间持久化存储一个值(该值变化不会触发组件重新渲染)。
使用场景
- 直接操作 DOM 元素(如聚焦输入框、获取元素尺寸)
- 存储不需要触发渲染的持久化数据(如定时器 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:复杂状态逻辑的管理
作用
useReducer
是 useState
的替代方案,用于管理包含多个子值的复杂状态逻辑,或当状态转换逻辑复杂且需要复用、预测时。它基于 Redux 的思想,通过"动作(action)"来描述状态变化,并使用" reducer 函数"来处理状态转换。
使用场景
- 状态逻辑复杂(如包含多个子状态,且状态更新依赖于前一个状态)
- 多个组件需要共享状态更新逻辑
- 需要预测和测试状态变化(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"问题)。
使用场景
- 多个组件需要访问同一数据(如用户信息、主题设置、语言偏好)
- 组件层级较深,通过 props 传递数据繁琐
- 非父子关系组件间的数据共享
代码示例
步骤 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
用于缓存"昂贵计算"的结果,避免在每次组件渲染时重复执行这些计算,从而优化性能。它接收一个计算函数和依赖数组,只有当依赖项发生变化时,才会重新执行计算函数并更新缓存结果。
使用场景
- 执行昂贵的计算操作(如大数据排序、复杂数据转换)
- 避免在每次渲染时创建新的对象/数组(导致子组件不必要的重渲染)
代码示例
基础用法:缓存昂贵计算结果
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
后,只有 name
或 age
变化时,user
对象才会更新,避免了因 count
变化导致的 UserCard
不必要重渲染。
useCallback:函数引用的缓存与性能优化
作用
useCallback
用于缓存函数的引用,避免在每次组件渲染时创建新的函数实例。它通常与 React.memo
配合使用,防止因函数 props 变化导致子组件不必要的重渲染。
使用场景
- 将函数作为 props 传递给子组件,且子组件使用
React.memo
优化 - 函数作为
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 更新后的同步操作
作用
useLayoutEffect
与 useEffect
功能类似,但执行时机不同:它在 DOM 更新后同步执行 (阻塞浏览器绘制),而 useEffect
在 DOM 更新后异步执行(不阻塞绘制)。
使用场景
- 需要读取 DOM 布局并立即执行操作(如测量元素尺寸后调整位置)
- 避免因异步执行导致的视觉闪烁(如模态框定位计算)
代码示例
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 以 :
开头,确保全局唯一性。
使用场景
- 为表单元素生成
id
和htmlFor
属性(关联 label 和 input) - 为无障碍(a11y)属性生成唯一标识符(如
aria-labelledby
) - 避免手动生成 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 会优先处理紧急更新(如输入框输入),延迟处理非紧急更新(如大型列表过滤),避免页面卡顿。
使用场景
- 大型列表过滤或排序(数据量大时避免阻塞 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 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:延迟更新低优先级值
作用
useDeferredValue
与 useTransition
类似,用于延迟更新"低优先级"的值,但它直接作用于值而非更新函数。当原值变化时,React 会先使用旧值渲染,待空闲后再更新为新值。
使用场景
- 显示大型列表的过滤结果(保持输入框响应性)
- 延迟更新非关键 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 并发渲染模式下数据的一致性和可预测性。
使用场景
- 订阅外部状态管理库(替代
useEffect
手动订阅) - 监听浏览器 API 变化(如
localStorage
、sessionStorage
) - 确保并发模式下的数据安全访问
代码示例
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
。
使用场景
- CSS-in-JS 库动态插入样式(如 styled-components、Emotion)
- 需要在 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 实例暴露出去,增强组件封装性。
使用场景
- 父组件需要调用子组件的特定方法(如表单提交、重置)
- 限制父组件可访问的子组件功能(避免直接操作 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。
使用场景
- 开发共享自定义 Hook(如
useLocalStorage
、useFetch
) - 增强自定义 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 最佳实践与注意事项
核心原则
- 只在顶层调用 Hooks:不要在循环、条件或嵌套函数中调用 Hooks,确保每次渲染时 Hooks 调用顺序一致。
- 只在函数组件或自定义 Hook 中调用:避免在普通 JavaScript 函数中使用 Hooks。
- 依赖数组要完整 :
useEffect
、useMemo
、useCallback
的依赖数组应包含所有外部变量,避免闭包陷阱。
性能优化建议
- 避免过度优化 :
useMemo
和useCallback
本身有性能开销,仅在确有性能问题时使用。 - 合理拆分组件:将复杂逻辑拆分为小型组件,减少不必要的重渲染。
- 使用 React.memo 谨慎 :仅对纯展示组件使用
React.memo
,避免浅比较成本高于重渲染成本。
常见陷阱
- 闭包陷阱 :
useEffect
中依赖项未更新时,内部函数可能捕获旧的状态值。 - 过度使用 Context:Context 变化会导致所有消费组件重渲染,复杂状态建议使用状态管理库。
- 忽略清理函数 :忘记清理
useEffect
中的订阅或定时器,可能导致内存泄漏。
通过合理使用和组合这些 Hooks,开发者可以编写出更简洁、可维护且高性能的 React 应用。React 18 新增的 Hooks 进一步增强了并发渲染下的用户体验,建议在项目中逐步实践和迁移。# React 18 新增 Hooks
useId:唯一 ID 生成器
作用
useId
是 React 18 引入的用于生成唯一 ID 的 Hook,特别适用于需要在服务端渲染 (SSR) 中避免 hydration 不匹配的场景。它生成的 ID 带有稳定的前缀,确保客户端和服务端生成的 ID 一致。
使用场景
- 为表单元素生成关联的
id
和htmlFor
属性 - 为无障碍 (a11y) 属性生成唯一标识符(如
aria-labelledby
) - 避免 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 卡顿,提升用户体验。
使用场景
- 大型列表过滤或排序(如搜索框输入过滤长列表)
- 复杂数据计算或转换(不希望阻塞用户输入)
- 任何可能导致 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:延迟更新低优先级值
作用
useDeferredValue
与 useTransition
类似,用于延迟更新低优先级的值。不同之处在于,useDeferredValue
是对值进行延迟,而 useTransition
是对更新函数进行延迟包装。
使用场景
- 当某个值的计算可能阻塞 UI,但又无法通过
useTransition
包装(如从 props 接收的值) - 需要基于延迟值进行渲染,且希望保持组件结构简洁
代码示例
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
),确保在并发渲染模式下数据的一致性和可预测性。
使用场景
- 订阅外部状态管理库(替代
useEffect
手动订阅) - 监听浏览器 API 变化(如
localStorage
、sessionStorage
) - 确保并发模式下的数据同步
代码示例
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)。
使用场景
- CSS-in-JS 库内部实现样式插入
- 需要在 DOM 元素渲染前注入关键样式
- 性能敏感的样式操作(避免布局偏移)
代码示例
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 最佳实践与注意事项
基础原则
- 只在函数组件或自定义 Hook 中调用 Hooks,不要在普通函数、循环或条件语句中调用
- 遵循依赖数组规则 :确保
useEffect
、useMemo
、useCallback
的依赖数组完整包含所有外部变量 - 避免过度优化 :
useMemo
和useCallback
有性能开销,仅在确实存在性能问题时使用
常见陷阱
- 闭包陷阱 :
useEffect
中访问的状态是捕获的渲染时的状态,如需最新状态可使用useRef
存储 - 依赖缺失:忘记在依赖数组中添加变量,导致副作用逻辑使用旧值
- 过度使用 Context :
useContext
会导致消费组件在 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 会不断出现,建议持续关注官方文档以获取最新信息。