为什么写这篇:React Hooks 2019-02 在 16.8 正式发布,5 年内彻底改变了 React 的写法。从 useState 到 React 19 的 useActionState,Hooks 已经从"新特性"变成"默认写法"。本文按"开箱即用频次"梳理 15 个核心 Hook,给出核心概念、最佳实践与常见坑。
适用读者:刚学 React Hooks 的同学;想系统梳理 18/19 新 Hook 的中级工程师;从 class 组件迁过来的老 React。
前置知识:理解 JSX、函数组件、props。
目录
- Hooks 设计哲学
- useState:状态管理基石
- useReducer:复杂状态的"action + reducer"模式
- useEffect:副作用与清理
- useLayoutEffect:DOM 更新同步完成后立即执行
- useContext:跨组件共享数据
- useRef:DOM 引用与持久化数据
- useImperativeHandle:自定义 ref 暴露的接口
- useMemo & useCallback:缓存计算结果与函数引用
- useDebugValue:自定义 Hook 在 DevTools 中的标签
- useId:跨服务端/客户端的唯一 ID
- useTransition:标记非紧急任务
- React 19 新 Hook 速览
- 自定义 Hook:组合已有 Hook 复用逻辑
- Hooks 三大规则
1. Hooks 设计哲学
Hooks 解决的核心问题:让函数组件拥有 class 组件的能力(state、生命周期、context),同时让代码更简洁、更好复用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // ❌ class 组件:逻辑散在生命周期里
class UserList extends React.Component {
state = { users: [] }
componentDidMount() {
fetch('/api/users').then(r => r.json()).then(users => this.setState({ users }))
}
render() {
return <ul>{this.state.users.map(u => <li>{u.name}</li>)}</ul>
}
}
// ✅ 函数组件 + Hook:逻辑聚拢、可复用
function UserList() {
const [users, setUsers] = useState([])
useEffect(() => {
fetch('/api/users').then(r => r.json()).then(setUsers)
}, [])
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
|
2. useState:状态管理基石
1
| const [count, setCount] = useState(0)
|
2.1 初始值是函数(昂贵计算)
1
2
3
4
5
6
7
8
9
10
| function HeavyComputationComponent() {
const expensiveInitialValue = () => {
let sum = 0
for (let i = 0; i < 1_000_000_000; i++) sum += i
return sum % 100
}
// 函数只在首次渲染执行一次
const [value, setValue] = useState(expensiveInitialValue)
return <div>{value}</div>
}
|
2.2 函数式更新
1
2
3
4
| function handleClickMultipleUpdates() {
setCount(prev => prev + 1)
setCount(prev => prev + 1) // 最终 count = 2(不是 1)
}
|
Why:React 18 的 setState 是批处理 + 异步的。连续调 3 次 setCount(c => c+1) 拿到的是"3 次累加后的 count"。
2.3 对象/数组不可变更新
1
2
3
4
5
6
7
| const [user, setUser] = useState({ name: '张三', age: 20 })
// ✅ 正确:展开旧对象 + 改一个属性
setUser(prev => ({ ...prev, age: prev.age + 1 }))
const [items, setItems] = useState(['苹果', '香蕉'])
setItems(prev => [...prev, '橙子'])
|
直接修改原对象不会触发重渲染——React 用引用比较判断要不要更新。
3. useReducer:复杂状态的"action + reducer"模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| const initialState = { count: 0, step: 1 }
function reducer(state, action) {
switch (action.type) {
case 'increment': return { ...state, count: state.count + state.step }
case 'decrement': return { ...state, count: state.count - state.step }
case 'setStep': return { ...state, step: action.payload }
default: throw new Error('unknown action')
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'setStep', payload: 5 })}>step=5</button>
</>
)
}
|
vs useState:state 之间有联动、依赖前一个 state、有多种"动作"时用 useReducer 更清晰。
4. useEffect:副作用与清理
1
2
3
4
5
6
7
8
9
| useEffect(() => {
// 副作用逻辑
console.log('组件已渲染或依赖项已更新')
return () => {
// 可选:清理函数
console.log('清理')
}
}, [dep1, dep2])
|
4.1 三个常见用法
| 依赖项 | 行为 |
|---|
| 不传 | 每次渲染后都执行(通常不要) |
[] | 只在挂载/卸载时执行(一次) |
[a, b] | a 或 b 变化时执行 |
4.2 清理函数:避免内存泄漏
1
2
3
4
| useEffect(() => {
const timer = setInterval(() => console.log('tick'), 1000)
return () => clearInterval(timer) // 卸载时清理
}, [])
|
常见需要清理的场景:定时器、事件监听、网络请求(AbortController)、WebSocket、订阅。
4.3 AbortController 取消请求
1
2
3
4
5
6
7
8
| useEffect(() => {
const controller = new AbortController()
fetch('/api/data', { signal: controller.signal })
.then(r => r.json())
.then(setData)
.catch(e => { if (e.name !== 'AbortError') console.error(e) })
return () => controller.abort()
}, [])
|
5. useLayoutEffect:DOM 更新同步完成后立即执行
1
2
3
4
5
| useLayoutEffect(() => {
// 同步执行,浏览器还没绘制
ref.current.style.opacity = '0'
// 立即读取布局相关数据
}, [data])
|
vs useEffect:
useEffect 异步,浏览器已绘制完才跑useLayoutEffect 同步,DOM 更新完、浏览器重绘前立即跑
适用场景:读 DOM 布局(offsetHeight、scrollTop)、强制同步重排避免视觉抖动。
陷阱:服务端渲染(SSR)会发警告。建议优先用 useEffect,只有当出现"渲染闪烁"时才换 useLayoutEffect。
6. useContext:跨组件共享数据
解决 “props 透传"问题——避免一层层 <A><B><C>...</C></B></A> 手动传 props。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 1. 创建 Context
const ThemeContext = createContext({ theme: 'light', toggle: () => {} })
// 2. Provider 包裹
function App() {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, toggle: () => setTheme(t => t === 'light' ? 'dark' : 'light') }}>
<Page />
</ThemeContext.Provider>
)
}
// 3. 子组件消费
function Button() {
const { theme, toggle } = useContext(ThemeContext)
return <button onClick={toggle}>{theme === 'light' ? '🌞' : '🌙'}</button>
}
|
坑:Context 的 value 每次渲染都是新对象,会让所有消费者重渲染。优化:用 useMemo 包装 value。
1
2
| const value = useMemo(() => ({ theme, toggle }), [theme])
return <ThemeContext.Provider value={value}>...</ThemeContext.Provider>
|
7. useRef:DOM 引用与持久化数据
7.1 获取 DOM 引用
1
2
3
4
5
6
7
8
9
| function TextInput() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus() // 自动聚焦
}, [])
return <input ref={inputRef} />
}
|
7.2 持久化数据(不触发重渲染)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| function Timer() {
const [count, setCount] = useState(0)
const intervalRef = useRef<number | null>(null)
const start = () => {
intervalRef.current = window.setInterval(() => {
setCount(c => c + 1)
}, 1000)
}
const stop = () => {
if (intervalRef.current) clearInterval(intervalRef.current)
}
return (
<>
<p>{count}s</p>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
</>
)
}
|
vs useState:改 ref.current 不触发 重渲染——适合"我需要存个值,但改了不更新 UI”。
8. useImperativeHandle:自定义 ref 暴露的接口
配合 forwardRef 使用,让父组件通过 ref 访问子组件的特定方法(隐藏内部实现)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null)
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => { if (inputRef.current) inputRef.current.value = '' }
}))
return <input ref={inputRef} {...props} />
})
// 父组件
function Parent() {
const ref = useRef<{ focus: () => void; clear: () => void }>(null)
return (
<>
<FancyInput ref={ref} />
<button onClick={() => ref.current?.focus()}>聚焦</button>
<button onClick={() => ref.current?.clear()}>清空</button>
</>
)
}
|
Why:子组件自己决定暴露什么 API,父组件不需要知道内部 DOM 结构。
9. useMemo & useCallback:缓存计算结果与函数引用
9.1 useMemo:缓存"计算结果"
1
2
3
| const expensiveValue = useMemo(() => {
return data.reduce((sum, item) => sum + item.price * item.qty, 0)
}, [data]) // data 变才重算
|
9.2 useCallback:缓存"函数引用"
1
2
3
4
5
6
7
8
| const handleClick = useCallback((id: string) => {
setItems(items => items.filter(i => i.id !== id))
}, []) // 函数永远不重创建
// 子组件配合 React.memo
const Child = memo(function Child({ onClick, value }) {
return <button onClick={() => onClick(value)}>click</button>
})
|
三者联动:useMemo 计算 + useCallback 传函数 + React.memo 阻止子组件重渲染——三件套只在大列表/复杂组件才有用。
常见误区:每个函数都包 useCallback——会拖慢性能(依赖比较也是开销)。只在该函数被传给 memo 化的子组件时才用。
1
2
3
4
5
6
| function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null)
// ... 副作用逻辑
useDebugValue(isOnline ? 'Online' : 'Offline')
return isOnline
}
|
React DevTools → Components 面板里这个 Hook 会显示 Online / Offline,方便调试。
仅在 DevTools 中显示,生产环境不占成本(React 会自动移除)。
11. useId:跨服务端/客户端的唯一 ID
解决 SSR 的水合(hydration)问题——服务端和客户端的 ID 必须一致,否则 React 警告。
1
2
3
4
5
6
7
8
9
| function PasswordField() {
const id = useId() // :r0:
return (
<>
<label htmlFor={id}>Password</label>
<input id={id} type="password" />
</>
)
}
|
vs 自增 ID:Math.random() / Date.now() 在 SSR 下会不一致。
12. useTransition:标记非紧急任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| function Search({ data }) {
const [isPending, startTransition] = useTransition()
const [filter, setFilter] = useState('')
const handleChange = (e) => {
// 紧急任务:更新输入框(必须立即响应)
setFilter(e.target.value)
// 非紧急任务:过滤大列表(可被打断)
startTransition(() => {
setFilteredData(data.filter(d => d.name.includes(e.target.value)))
})
}
return (
<>
<input value={filter} onChange={handleChange} />
{isPending && <Spinner />}
<List items={filteredData} />
</>
)
}
|
效果:用户输入时,输入框立即响应;过滤计算在后台进行(“transitioning”),避免页面卡顿。
13. React 19 新 Hook 速览
13.1 useActionState(替代 useState 表单提交)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| async function saveAction(prevState, formData) {
await fetch('/api/save', { method: 'POST', body: formData })
return { ok: true }
}
function Form() {
const [state, formAction, isPending] = useActionState(saveAction, { ok: false })
return (
<form action={formAction}>
<input name="title" />
<button disabled={isPending}>{isPending ? '保存中...' : '保存'}</button>
{state.ok && <p>已保存</p>}
</form>
)
}
|
省掉手写 useState({ loading, error, data }) 三个状态——React 自动管。
1
2
3
4
| function SubmitButton() {
const { pending, data, method } = useFormStatus()
return <button disabled={pending}>{pending ? '...' : '提交'}</button>
}
|
13.3 useOptimistic(乐观更新)
1
2
3
4
5
6
7
8
9
10
11
12
| function Like({ likes, isLiked }) {
const [optimistic, setOptimistic] = useOptimistic(isLiked)
return (
<button onClick={async () => {
setOptimistic(true) // UI 立即显示已赞
await fetch('/api/like', { method: 'POST' })
// 接口成功后由父组件更新真实状态
}}>
{optimistic ? '❤️' : '🤍'} {likes}
</button>
)
}
|
Why:用户点"赞"后接口还没返回时,按钮已经变成红色——体感流畅 5 倍。
14. 自定义 Hook:组合已有 Hook 复用逻辑
自定义 Hook = 一个以 use 开头的函数,内部调用其他 Hook。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // useLocalStorage.ts
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key)
return stored ? JSON.parse(stored) : initialValue
})
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue] as const
}
// 用法
function App() {
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')
return <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>{theme}</button>
}
|
常见自定义 Hook:useFetch / useDebounce / useThrottle / usePagination / useModal / useWebSocket。
15. Hooks 三大规则
- 只能在函数组件顶层调用——不能在 if/for/嵌套函数里
- 只能在 React 函数中调用——函数组件、自定义 Hook;不能在普通 JS 函数里
- useEffect 依赖项必须完整——配合
eslint-plugin-react-hooks 自动检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // ❌ 错误:条件调用 Hook
function Bad() {
if (count > 0) {
const [v, setV] = useState(0) // 报错:Hook 数量不固定
}
}
// ✅ 正确:永远在顶层调用
function Good() {
const [v, setV] = useState(0)
if (count > 0) {
// 用 v 即可
}
}
|
Why:React 用"调用顺序"识别每个 Hook(第一次是 useState、第二次是 useEffect…)。条件调用会破坏顺序,state 会错位。
小结
React Hooks 核心 15 个:
| Hook | 用途 | React 版本 |
|---|
useState | 状态管理基石 | 16.8+ |
useReducer | 复杂状态 | 16.8+ |
useEffect | 副作用 | 16.8+ |
useLayoutEffect | DOM 同步副作用 | 16.8+ |
useContext | 跨组件共享 | 16.8+ |
useRef | DOM 引用 + 持久化数据 | 16.8+ |
useImperativeHandle | 配合 forwardRef 暴露 API | 16.8+ |
useMemo / useCallback | 性能优化 | 16.8+ |
useDebugValue | DevTools 标签 | 16.8+ |
useId | SSR 唯一 ID | 18+ |
useTransition | 标记非紧急任务 | 18+ |
useActionState | 表单状态管理 | 19+ |
useFormStatus | 读表单状态 | 19+ |
useOptimistic | 乐观更新 | 19+ |
useDeferredValue | 延迟更新值 | 18+ |
下一步:学完 Hooks 之后,进阶性能优化(React.memo / 虚拟列表 / 代码分割)、状态管理(Redux Toolkit / Zustand / Jotai)、数据请求(React Query / SWR)、渲染模式(Suspense / Streaming SSR / Server Components)。
参考资料