系统整理 React 常用 Hooks 的用法与实战示例,涵盖状态管理、副作用处理、性能优化、DOM 操作等维度,代码与注释完整保留。
一、状态管理
1. useState
useState 是 React 中最基础的状态管理 Hook,用于在函数组件中添加状态。
核心要点:
- 普通形式 :
const [state, setState] = useState(initialValue) - 函数形式 :
const [state, setState] = useState(() => initialValue),适用于初始值需要计算的场景 - 批量更新 :React 18 中所有更新皆为异步批量处理,多次
setState会合并 - 函数式更新 :
setState(prev => prev + 1),基于上一次状态计算新值,不会被合并
tsx
import { Button, Flex } from 'antd';
import React, { useState } from 'react';
import { flushSync } from 'react-dom';
const UseStateComponent: React.FC = () => {
const [count, setCount] = useState<number>(0); // 普通
// set 属于异步更新 - 即在函数中多次使用 set 会合并操作;
// const [count, setCount] = useState<number>(() => 0) // 函数式
// + 1 在 react18之前合成事件为异步更新,原生事件是同步更新
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
};
// + 1 但在 react18 皆为异步更新
const handleTimeoutClick = () => {
setTimeout(() => setCount(count + 1));
setTimeout(() => setCount(count + 1));
};
// +2 可以使用函数形式;函数无法合并,即每次返回上一次周期的值;
const handleFucClick = () => {
setCount((count) => count + 1);
setCount((count) => count + 1);
};
// +2 强制同步更新
const handleSyncClick = () => {
flushSync(() => setCount((count) => count + 1));
flushSync(() => setCount((count) => count + 1));
};
return (
<Flex gap={5} vertical>
<p>{count}</p>
<Flex gap={5}>
<Button onClick={handleClick}>+ 1</Button>
<Button onClick={handleTimeoutClick}>setTimeout + 1</Button>
<Button onClick={handleFucClick}>func + 2</Button>
<Button onClick={handleSyncClick}>flushSync + 2</Button>
</Flex>
</Flex>
);
};
export default UseStateComponent;
四种更新方式对比:
| 方式 | 行为 | 结果 |
|---|---|---|
handleClick |
两次 setCount(count + 1) |
+1(合并) |
handleTimeoutClick |
setTimeout 中两次 set | React 18 仍为 +1(合并) |
handleFucClick |
函数式更新 | +2(不合并) |
handleSyncClick |
flushSync 强制同步 |
+2(立即执行) |
2. useReducer
useReducer 是 useState 的加强版,适用于状态逻辑复杂 、或下一个状态依赖前一个状态的场景。
核心要点:
- 使用
reducer函数集中管理状态变更逻辑 - 通过
dispatch派发 action 来触发状态更新 - 代码结构更清晰,便于测试和维护
tsx
import { Button, Typography } from 'antd'
import { useReducer } from 'react'
const initState = { count: 0 }
// 相当于 useState 加大版,用于处理更庞大且复杂的数据
const reducer = (state = initState, action: any) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count + -1 }
default:
return state
}
}
const UseReducerComponent = () => {
const [state, dispatch] = useReducer(reducer, initState)
return (
<Typography>
<Typography.Text>{state.count}</Typography.Text>
<Button onClick={() => dispatch({ type: 'increment' })}>+</Button>
<Button onClick={() => dispatch({ type: 'decrement' })}>-</Button>
</Typography>
)
}
export default UseReducerComponent
适用场景:
- 状态对象包含多个子属性
- 状态更新逻辑复杂(不止简单的
setXxx) - 需要复用状态逻辑到多个组件
useState vs useReducer:
| 维度 | useState | useReducer |
|---|---|---|
| 复杂度 | 简单状态 | 复杂状态逻辑 |
| 可读性 | 简单场景清晰 | 复杂场景更清晰 |
| 复用性 | 需自定义 Hook | reducer 可独立复用 |
| 调试 | 直接查看 state | 通过 action 追溯变更 |
二、副作用处理
3. useEffect
useEffect 是 React 中最常用的副作用 Hook,在组件渲染到屏幕之后执行。
核心要点:
- 无依赖数组 :每次渲染后都执行(
DidMount + DidUpdate) - 空依赖数组
[]:只在组件挂载时执行(模拟componentDidMount) - 有依赖数组
[dep]:挂载时 + 依赖变化时执行(模拟DidMount + 关联 DidUpdate) - 返回清理函数 :组件卸载时执行(模拟
componentWillUnmount,但不完全等同)
tsx
import { Button } from 'antd'
import React, { useEffect, useState } from 'react'
const UseEffectComponent: React.FC = () => {
const [count, setCount] = useState(0)
// 没有依赖时: DidMount + DidUpdate
// 组件初始化和 props 更新都会执行;
useEffect(() => {
console.log('没有任何依赖')
})
// 依赖为空值相当于 DidMount;
useEffect(() => {
console.log('依赖为空值')
}, [])
// 存在依赖时,相当于 DidMount + 关联依赖 DidUpdate ;
useEffect(() => {
console.log('存在依赖')
}, [count])
// return 相当于模拟willUnMount但不等于;
useEffect(() => {
console.log('开始')
return () => {
console.log('结束')
}
}, [])
return <Button onClick={() => setCount(count + 1)}>点击</Button>
}
export default UseEffectComponent
四种依赖模式对比:
| 依赖形式 | 执行时机 | 用途 |
|---|---|---|
无 [] |
每次渲染后 | 监听所有变化 |
[] |
仅挂载时 | 初始化操作(如请求数据) |
[dep] |
挂载 + 依赖变化 | 依赖特定状态/属性 |
| return fn | 卸载时(或依赖变化前) | 清理副作用(取消订阅、清除定时器等) |
4. useLayoutEffect
useLayoutEffect 与 useEffect 签名相同,但执行时机不同:在浏览器绘制之前同步执行。
核心要点:
- 执行时机:DOM 变更后、浏览器绘制前同步执行
- 适用场景:需要读取/修改 DOM 布局(如测量元素尺寸、位置)
- 注意:同步执行会阻塞浏览器绘制,谨慎使用,避免性能问题
tsx
import React, { useLayoutEffect, useRef, useState } from 'react';
/**
* useLayoutEffect 会在浏览器绘制前同步执行,适合在这里订阅布局相关的外部系统(如 ResizeObserver)。
* 在 ResizeObserver 回调里 setState,属于「外部系统通知再更新」,避免在 effect 主体内同步 setState。
*/
const UseLayoutEffectComponent: React.FC = () => {
const textRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
const el = textRef.current;
if (!el) return;
const update = () => {
setWidth(Math.round(el.getBoundingClientRect().width));
};
const ro = new ResizeObserver(update);
ro.observe(el);
return () => {
ro.disconnect();
};
}, []);
return (
<div>
<div ref={textRef} style={{ display: 'inline-block', fontWeight: 600 }}>
useLayoutEffect 读取真实宽度
</div>
<p>当前宽度:{width}px</p>
</div>
);
};
export default UseLayoutEffectComponent;
useEffect vs useLayoutEffect:
| 维度 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | 绘制后异步执行 | 绘制前同步执行 |
| 阻塞渲染 | 否 | 是(可能导致闪烁减少但性能下降) |
| 适用场景 | 大多数副作用(请求、事件订阅等) | DOM 测量、布局调整 |
| 视觉闪烁 | 可能出现 | 避免布局闪烁 |
💡 建议 :优先使用
useEffect,只有当出现布局闪烁问题时再考虑useLayoutEffect。
三、性能优化
5. useMemo
useMemo 用于缓存计算结果 ,只有当依赖项发生变化时才重新计算。类似于 Vue 中的 computed。
核心要点:
- 缓存昂贵的计算结果,避免每次渲染都重新计算
- 第二个参数为依赖数组,只有依赖变化时才重新执行
- 不要滥用,简单计算无需使用
tsx
import { Button } from 'antd'
import React, { useMemo, useState } from 'react'
const list = [1, 3, 5, 7, 9]
/**
* 当组件中的某一个值发生改变时,如果存在需要计算的值会重新进行计算;
* 而 useMemo 可以缓存计算结果(类似 vue computed),只有在依赖项发生变化时重新计算
*/
const UseMemoComponent: React.FC = () => {
const [count, setCount] = useState(0)
// 每次点击按钮(setCount)会重新渲染此函数
const totalNormal = () => {
console.log('totalNormal')
return list.reduce((prev, cur) => prev + cur)
}
// 类似 computed,有缓存,[] 关联属性才会重新执行
const total = useMemo(() => {
console.log('useMemo')
return list.reduce((prev, cur) => prev + cur)
}, [])
return (
<div>
<p>{count}</p>
<p>normal:{totalNormal()}</p>
<p>useMemo:{total}</p>
<Button onClick={() => setCount(count + 1)} type="primary">
点击
</Button>
</div>
)
}
export default UseMemoComponent
对比:
| 方式 | 行为 | 输出 |
|---|---|---|
totalNormal() |
每次渲染都重新计算 | 25,但每次都执行 console.log |
useMemo |
依赖不变时返回缓存值 | 25,只在依赖变化时执行计算 |
6. useCallback
useCallback 用于缓存函数引用 ,配合 React.memo 使用可以避免子组件不必要的重渲染。
核心要点:
- 缓存函数本身(而非执行结果)
- 父组件传入子组件的函数 props 时,使用
useCallback避免子组件因函数引用变化而重渲染 - 需配合
memo包裹子组件才能生效 - 注意闭包陷阱:依赖未正确声明时,函数内可能捕获旧值
tsx
import { Button, Card, Col, Row, Typography } from 'antd'
import React, { memo, useCallback, useEffect, useState } from 'react'
import type { ChangeEvent } from 'react'
interface ChildProps {
onchange: (e: ChangeEvent<HTMLInputElement>) => void
}
/**
* 左侧:useCallback + memo 缓存函数示例
* 父组件传入子组件函数;当父组件发生变化,子组件也会重新渲染,将函数缓存配合 memo 使用;
* 使用 memo 进行包裹缓存组件,相当于 PureComponent
*/
const MemoCacheDemo: React.FC = () => {
const [count, setCount] = useState(0)
// 点击子组件不会重新渲染
const onchange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value)
}, [])
return (
<Card title="useCallback + memo 缓存函数" bordered={false}>
<Typography.Title level={4}>{count}</Typography.Title>
<Button onClick={() => setCount(count + 1)}>+1</Button>
<div style={{ marginTop: 16 }}>
<Child onchange={onchange} />
</div>
</Card>
)
}
const Child = memo<ChildProps>(({ onchange }) => {
console.log('子组件渲染~~')
return <input type="text" onChange={onchange} />
})
/**
* 右侧:闭包陷阱示例
* 如果版本是 v17 这是一个有问题的自定义钩子
*/
const ClosureTrapDemo: React.FC = () => {
const [count, setCount] = useState(0)
const handleClick = useCallback(() => {
console.log(count) // 第一次是 0 如果 count 改变,handleClick 会保留旧值
}, [count])
useEffect(() => {
setTimeout(() => {
setCount((prev) => prev + 1)
}, 1000)
}, [])
return (
<Card title="闭包陷阱" bordered={false}>
<Typography.Title level={4}>当前计数:{count}</Typography.Title>
<Button onClick={handleClick}>点击计数(查看控制台)</Button>
</Card>
)
}
const UseCallbackComponent: React.FC = () => {
return (
<Row gutter={24}>
<Col span={12}>
<MemoCacheDemo />
</Col>
<Col span={12}>
<ClosureTrapDemo />
</Col>
</Row>
)
}
export default UseCallbackComponent
两个示例说明:
| 示例 | 说明 |
|---|---|
| MemoCacheDemo | useCallback + memo 配合,父组件状态更新不会触发子组件重新渲染 |
| ClosureTrapDemo | 闭包陷阱演示,useCallback 的依赖数组未正确维护时,函数内捕获的是旧值 |
7. useTransition
useTransition 用于将低优先级状态更新标记为"过渡",让高优先级更新(如用户输入)优先响应,避免界面卡顿。
核心要点:
startTransition:将状态更新包裹为过渡更新isPending:布尔值,表示过渡是否进行中,可用于展示 loading 状态- 适用于:大量数据过滤、列表渲染等耗时操作
tsx
import { Input, Spin } from 'antd'
import React, { useState, useTransition } from 'react'
/**
* 用于在处理异步操作时提供平滑的过渡效果;
* 可以帮助你在组件的状态变化时,以动画的方式过渡到新的状态。
*/
const UseTransitionComponent: React.FC = () => {
const [list, setList] = useState<Array<number | string>>([])
const [isPending, startTransition] = useTransition() // 更新过程
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const arr = new Array(5).fill(e.target.value)
startTransition(() => {
setList([...list, ...arr])
})
}
return (
<>
<Input type="text" onChange={handleChange} />
<>
{isPending ? (
<Spin>loading...</Spin>
) : (
list.map((item, i) => {
return <b key={i}>{item}</b>
})
)}
</>
</>
)
}
export default UseTransitionComponent
8. useDeferredValue
useDeferredValue 用于将某个状态值延迟更新,让 React 优先渲染紧急更新(如用户输入),再处理基于该值的昂贵计算/渲染。
核心要点:
- 接收一个值,返回该值的"延迟版本"
- 当紧急更新(如输入)发生时,先使用旧值渲染,稍后再用新值重新渲染
- 适用于:搜索过滤、大数据列表等场景
- 与
useTransition的区别:useDeferredValue作用于值 ,useTransition作用于状态更新函数
tsx
import { Input } from 'antd'
import React, { useDeferredValue, useMemo, useState } from 'react'
const mockData = Array.from({ length: 800 }, (_, i) => `React Hook Item ${i + 1}`)
/**
* useDeferredValue:把高开销渲染延后,优先保证输入框流畅。
*/
const UseDeferredValueComponent: React.FC = () => {
const [keyword, setKeyword] = useState('')
const deferredKeyword = useDeferredValue(keyword)
const result = useMemo(() => {
const lowCaseKeyword = deferredKeyword.trim().toLowerCase()
if (!lowCaseKeyword) return mockData.slice(0, 30)
return mockData.filter((item) => item.toLowerCase().includes(lowCaseKeyword)).slice(0, 50)
}, [deferredKeyword])
return (
<div>
<Input
placeholder="输入关键字试试(输入仍然会比较顺滑)"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
style={{ maxWidth: 320, marginBottom: 12 }}
/>
<div>命中数量:{result.length}</div>
<div style={{ maxHeight: 200, overflow: 'auto', marginTop: 8 }}>
{result.map((item) => (
<div key={item}>{item}</div>
))}
</div>
</div>
)
}
export default UseDeferredValueComponent
useTransition vs useDeferredValue:
| 维度 | useTransition | useDeferredValue |
|---|---|---|
| 操作对象 | 状态更新函数 | 状态值 |
| 使用方式 | startTransition(() => setXxx(...)) |
const deferred = useDeferredValue(value) |
| 适用场景 | 主动控制哪些更新是过渡 | 将某个 props/state 延迟传递给子组件 |
| 获取 pending 状态 | isPending |
无(可用 deferred !== value 判断) |
性能优化 Hooks 对比总结:
| Hook | 缓存对象 | 解决什么问题 |
|---|---|---|
useMemo |
计算结果 | 避免昂贵计算重复执行 |
useCallback |
函数引用 | 避免子组件因函数引用变化而重渲染 |
useTransition |
状态更新 | 让紧急更新优先,避免界面卡顿 |
useDeferredValue |
状态值 | 延迟非紧急渲染,保证输入流畅 |
四、Refs 与 DOM 操作
9. useRef
useRef 用于在函数组件中获取对 DOM 元素或任意可变值的引用,其 .current 属性在组件重新渲染时保持不变。
核心要点:
- 返回一个可变的 ref 对象,
.current属性被初始化为传入的参数 - ref 的变更不会触发组件重新渲染
- 常见用途:获取 DOM 元素、保存上一次状态/属性、保存定时器 ID 等
tsx
import React, { useEffect, useRef } from 'react'
// 获取真实 dom 元素
const UseRefComponent: React.FC = () => {
const userRef = useRef<HTMLInputElement>(null)
useEffect(() => {
console.log(userRef.current)
userRef.current?.focus() // 获取焦点
}, [])
return <input type="text" ref={userRef} />
}
export default UseRefComponent
常见用途:
| 用途 | 示例 |
|---|---|
| 获取 DOM | const ref = useRef<HTMLInputElement>(null) |
| 自动聚焦 | ref.current?.focus() |
| 保存上一次的值 | const prevValue = useRef(value) |
| 保存定时器/订阅 ID | const timerRef = useRef<NodeJS.Timeout>() |
10. useImperativeHandle
useImperativeHandle 用于自定义通过 ref 暴露给父组件的实例值。通常与 forwardRef 配合使用。
核心要点:
- 配合
forwardRef将子组件的 DOM 或自定义方法暴露给父组件 - 父组件通过
ref调用子组件的方法(如focus()、clear()) - 避免了直接暴露整个 DOM 节点,只暴露必要的 API
tsx
import { Button } from 'antd'
import React, { forwardRef, useImperativeHandle, useRef } from 'react'
interface FocusInputRef {
focus: () => void
clear: () => void
}
const FocusInput = forwardRef<FocusInputRef>((_, ref) => {
const innerRef = useRef<HTMLInputElement>(null)
useImperativeHandle(ref, () => ({
focus: () => innerRef.current?.focus(),
clear: () => {
if (innerRef.current) innerRef.current.value = ''
},
}))
return (
<input
ref={innerRef}
placeholder="父组件通过 ref 调用 focus/clear"
style={{ width: 320, height: 32, padding: '4px 8px' }}
/>
)
})
FocusInput.displayName = 'FocusInput'
const UseImperativeHandleComponent: React.FC = () => {
const focusInputRef = useRef<FocusInputRef>(null)
return (
<div>
<FocusInput ref={focusInputRef} />
<div style={{ marginTop: 12, display: 'flex', gap: 8 }}>
<Button type="primary" onClick={() => focusInputRef.current?.focus()}>
聚焦输入框
</Button>
<Button onClick={() => focusInputRef.current?.clear()}>清空内容</Button>
</div>
</div>
)
}
export default UseImperativeHandleComponent
工作流程:
scss
父组件 子组件 (FocusInput)
│ │
├─── ref={focusInputRef} ─────────>│
│ ├── forwardRef 接收 ref
│ ├── useImperativeHandle 定义暴露的 API
│ │ ├── focus()
│ │ └── clear()
│ └── innerRef 绑定真实 <input>
│ │
├─── ref.current.focus() ────────>│ 调用暴露的方法
│ └── 实际操作 innerRef.current
注意事项:
- 必须配合
forwardRef使用,否则父组件无法传递 ref - 建议给 forwarded 组件设置
displayName,便于调试 - 优先使用 props 进行数据流传递,
useImperativeHandle是命令式操作的补充
useRef vs useImperativeHandle:
| 维度 | useRef | useImperativeHandle |
|---|---|---|
| 作用对象 | 当前组件内的 DOM/值 | 子组件暴露的 API |
| 配合使用 | 独立使用 | 必须配合 forwardRef |
| 数据流 | 单向(自己用) | 父子组件命令式通信 |
| 典型场景 | 聚焦、测量 DOM | 封装表单组件、封装播放器组件 |
五、其他
11. useId
useId 用于生成稳定且唯一 的 id,主要用于无障碍访问(a11y)场景中 label 与表单元素的关联。
核心要点:
- 生成在服务端和客户端之间保持稳定的唯一 ID
- 避免手写 id 可能导致的 SSR 水合(hydration)不匹配问题
- 适用于:
<label htmlFor>与<input id>的绑定
tsx
import { Input } from 'antd'
import React, { useId } from 'react'
/**
* useId:生成稳定且唯一的 id,适合表单标签关联。
*/
const UseIdComponent: React.FC = () => {
const inputId = useId()
return (
<div>
<label htmlFor={inputId} style={{ display: 'block', marginBottom: 8 }}>
用户名(label 与 input 绑定)
</label>
<Input id={inputId} placeholder="请输入用户名" style={{ maxWidth: 280 }} />
<p style={{ marginTop: 8, color: '#666' }}>当前 id: {inputId}</p>
</div>
)
}
export default UseIdComponent
为什么不用 Math.random() 或自增 ID?
| 方式 | 问题 |
|---|---|
Math.random() |
SSR 时服务端和客户端生成的 id 不一致,导致水合失败 |
| 全局自增计数器 | 多组件实例、并发渲染下可能冲突 |
useId |
React 内部保证唯一性和跨端一致性 ✅ |
生成的 id 格式示例:
makefile
:rc1:
:rc2:
:rc3:
格式可能随 React 版本变化,不要依赖其具体形式,只需保证唯一性即可。
整理自 @src/pages/react-hooks/,代码未删减,完整保留原始注释与实现。