React Hooks 深度解析与最佳实践指南
概述
React Hooks 在 React 16.8 版本推出,彻底改变了 React 组件的开发方式。它让开发者能够在不编写 class 组件的情况下使用 state 和其他 React 特性。本文将深入讲解 Hooks 的工作原理、常见陷阱和最佳实践。
为什么需要 Hooks?
Class 组件存在的问题
在 Hooks 出现之前,React 主要使用 class 组件:
// ❌ Class 组件的问题
class UserComponent extends React.Component {
state = {
user: null,
loading: true,
error: null
}
componentDidMount() {
// 获取用户数据
fetchUser().then(user => {
this.setState({ user, loading: false })
})
// 订阅用户状态
subscribeToUser(user => {
this.setState({ user })
})
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.refetch()
}
}
componentWillUnmount() {
// 清理订阅
unsubscribeFromUser()
}
refetch = () => {
// 重新获取数据
}
render() {
const { user, loading, error } = this.state
return (/* JSX */)
}
}
问题:
- 逻辑分散在各个生命周期方法中
- 相关逻辑被分割(订阅在 mount,取消订阅在 unmount)
- this 的绑定问题
- 代码重用需要高阶组件或 render props
- 需要学习 class、原型链等概念
Hooks 的优势
// ✅ 使用 Hooks 的优雅写法
function UserComponent({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
// 相关逻辑组织在一起
useEffect(() => {
let unsubscribe
setLoading(true)
fetchUser(userId)
.then(userData => {
setUser(userData)
// 订阅逻辑
unsubscribe = subscribeToUser(userData => {
setUser(userData)
})
})
.catch(err => setError(err))
.finally(() => setLoading(false))
// 清理函数
return () => unsubscribe?.()
}, [userId]) // 依赖数组
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return <div>{user?.name}</div>
}
核心 Hooks 详解
1. useState - 状态管理
useState 是最基础的 Hook,用于在函数组件中添加状态。
import { useState } from 'react'
function Counter() {
// 基础用法
const [count, setCount] = useState(0)
// 可以使用多个 useState
const [name, setName] = useState('')
const [items, setItems] = useState([])
// 复杂状态
const [form, setForm] = useState({
email: '',
password: ''
})
// 根据前一个状态更新
const increment = () => {
setCount(prevCount => prevCount + 1)
}
// 批量更新状态
const updateForm = (field, value) => {
setForm(prevForm => ({
...prevForm,
[field]: value
}))
}
return (
<div>
<button onClick={increment}>Count: {count}</button>
<input
value={form.email}
onChange={(e) => updateForm('email', e.target.value)}
/>
</div>
)
}
最佳实践:
// ✅ 保持状态粒度小
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
// ❌ 避免将不相关的状态放在一起
const [user, setUser] = useState({
firstName: '',
lastName: '',
notifications: true,
theme: 'dark'
})
// ✅ 使用状态更新函数而不是直接修改
const updateCount = () => {
setCount(c => c + 1) // 正确
// 而不是 setCount(count + 1) - 可能会有闭包问题
}
2. useEffect - 副作用处理
useEffect 用于处理副作用(数据获取、订阅、DOM 操作等)。
import { useEffect, useState } from 'react'
function DataFetcher() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
// Effect 1: 组件挂载时运行一次
useEffect(() => {
console.log('Component mounted')
}, []) // 空依赖数组
// Effect 2: 在数据变化时运行
useEffect(() => {
setLoading(true)
fetch('/api/data')
.then(res => res.json())
.then(json => {
setData(json)
setLoading(false)
})
.catch(err => {
setError(err)
setLoading(false)
})
}, []) // 仅在挂载时运行
// Effect 3: 依赖变化时运行
useEffect(() => {
// 监听窗口尺寸变化
const handleResize = () => {
console.log('Window resized')
}
window.addEventListener('resize', handleResize)
// 清理函数
return () => {
window.removeEventListener('resize', handleResize)
}
}, []) // 依赖数组
return <div>{loading ? 'Loading...' : JSON.stringify(data)}</div>
}
依赖数组的三种情况:
| 依赖数组 | 运行时机 | 用途 |
|---|---|---|
[] | 仅挂载和卸载 | 初始化、一次性订阅 |
[dep] | 挂载和 dep 变化 | 监听特定数据变化 |
| 无依赖 | 每次渲染后 | 不推荐,易造成性能问题 |
3. useContext - 跨组件通信
useContext 让你能够订阅 React context,避免 prop drilling。
import { createContext, useContext, useState } from 'react'
// 创建 Context
const ThemeContext = createContext()
// 提供者组件
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// 消费者组件
function ThemedComponent() {
const { theme, toggleTheme } = useContext(ThemeContext)
return (
<div style={{
background: theme === 'light' ? '#fff' : '#000',
color: theme === 'light' ? '#000' : '#fff'
}}>
<p>Current theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
)
}
// 应用使用
function App() {
return (
<ThemeProvider>
<ThemedComponent />
</ThemeProvider>
)
}
4. useReducer - 复杂状态逻辑
对于复杂的状态更新逻辑,useReducer 比 useState 更合适。
import { useReducer } from 'react'
// Reducer 函数
function todoReducer(state, action) {
switch (action.type) {
case 'ADD':
return {
todos: [...state.todos, { id: Date.now(), text: action.payload }],
count: state.count + 1
}
case 'REMOVE':
return {
todos: state.todos.filter(t => t.id !== action.payload),
count: state.count - 1
}
case 'TOGGLE':
return {
todos: state.todos.map(t =>
t.id === action.payload ? { ...t, done: !t.done } : t
),
count: state.count
}
default:
return state
}
}
function TodoApp() {
const initialState = { todos: [], count: 0 }
const [state, dispatch] = useReducer(todoReducer, initialState)
return (
<div>
<button onClick={() => dispatch({ type: 'ADD', payload: 'New task' })}>
Add Todo
</button>
{state.todos.map(todo => (
<div key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => dispatch({ type: 'TOGGLE', payload: todo.id })}
/>
<span>{todo.text}</span>
<button onClick={() => dispatch({ type: 'REMOVE', payload: todo.id })}>
Delete
</button>
</div>
))}
<p>Total: {state.count}</p>
</div>
)
}
高级 Hooks
useCallback - 缓存函数
useCallback 缓存函数引用,在子组件使用 React.memo 时防止不必要的重新渲染。
import { useCallback, useState } from 'react'
function Parent() {
const [count, setCount] = useState(0)
// 不使用 useCallback,每次渲染都创建新函数
// const handleClick = () => {
// console.log('Clicked')
// }
// 使用 useCallback,依赖不变时函数引用不变
const handleClick = useCallback(() => {
console.log(`Count is: ${count}`)
}, [count])
return (
<div>
<Child onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>Increment: {count}</button>
</div>
)
}
const Child = React.memo(({ onClick }) => {
return <button onClick={onClick}>Child Button</button>
})
useMemo - 缓存计算结果
useMemo 缓存昂贵计算的结果。
import { useMemo, useState } from 'react'
function ExpensiveCalculation() {
const [count, setCount] = useState(0)
const [multiplier, setMultiplier] = useState(1)
// 计算量大的操作
const expensiveValue = useMemo(() => {
console.log('Calculating...')
let result = 0
for (let i = 0; i < 1000000000; i++) {
result += i
}
return result * multiplier
}, [multiplier]) // 仅在 multiplier 变化时重新计算
return (
<div>
<p>Result: {expensiveValue}</p>
<button onClick={() => setCount(count + 1)}>
Rerender ({count})
</button>
<button onClick={() => setMultiplier(multiplier + 1)}>
Multiply ({multiplier})
</button>
</div>
)
}
自定义 Hooks
自定义 Hooks 让你能够复用有状态逻辑。
// hooks/useLocalStorage.js
import { useState, useEffect } from 'react'
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.error(error)
return initialValue
}
})
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.error(error)
}
}
return [storedValue, setValue]
}
// 使用
function MyComponent() {
const [name, setName] = useLocalStorage('name', '')
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Your name"
/>
)
}
// hooks/useFetch.js
import { useState, useEffect } from 'react'
function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let isMounted = true
const fetchData = async () => {
try {
const response = await fetch(url)
const json = await response.json()
if (isMounted) {
setData(json)
}
} catch (err) {
if (isMounted) {
setError(err)
}
} finally {
if (isMounted) {
setLoading(false)
}
}
}
fetchData()
return () => {
isMounted = false
}
}, [url])
return { data, loading, error }
}
// 使用
function UserProfile({ id }) {
const { data: user, loading, error } = useFetch(`/api/users/${id}`)
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return <div>{user?.name}</div>
}
常见陷阱与解决方案
陷阱 1: 闭包问题
// ❌ 错误:闭包陷阱
function Counter() {
const [count, setCount] = useState(0)
const handleClick = () => {
setTimeout(() => {
console.log(count) // 总是输出 0
}, 1000)
}
return <button onClick={handleClick}>{count}</button>
}
// ✅ 正确:使用更新函数获取最新值
function Counter() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(c => {
setTimeout(() => {
console.log(c) // 输出正确的值
}, 1000)
return c
})
}
return <button onClick={handleClick}>{count}</button>
}
陷阱 2: 依赖数组遗漏
// ❌ 错误:遗漏依赖
function UserProfile({ userId }) {
useEffect(() => {
fetchUser(userId) // 依赖了 userId,但没有在依赖数组中声明
}, []) // userId 变化时不会重新获取
}
// ✅ 正确:完整的依赖数组
function UserProfile({ userId }) {
useEffect(() => {
fetchUser(userId)
}, [userId]) // 当 userId 变化时重新运行
}
陷阱 3: 条件中的 Hooks
// ❌ 错误:在条件中使用 Hooks
function BadComponent({ isVisible }) {
if (isVisible) {
const [state, setState] = useState(0) // 违反规则
}
}
// ✅ 正确:Hooks 在顶层调用
function GoodComponent({ isVisible }) {
const [state, setState] = useState(0)
if (!isVisible) {
return null
}
return <div>{state}</div>
}
最佳实践总结
- 只在顶层调用 Hooks:不要在循环、条件或嵌套函数中调用 Hooks
- 完整的依赖数组:使用 ESLint 插件检查依赖数组的完整性
- 合理提取自定义 Hooks:当逻辑可复用时,提取为自定义 Hook
- 注意性能优化:谨慎使用 useCallback 和 useMemo,避免过度优化
- 处理异步操作的清理:在 useEffect 中处理异步时,返回清理函数防止内存泄漏
- 使用 TypeScript:为 Hooks 添加类型注解,提高代码质量
总结
React Hooks 通过将相关逻辑组织在一起,让代码更易维护和复用。掌握 Hooks 的工作原理和常见陷阱是编写高效 React 应用的基础。
关键要点
- ✅ useState 用于管理状态
- ✅ useEffect 用于处理副作用
- ✅ 依赖数组控制 Effect 的执行时机
- ✅ 自定义 Hooks 提高代码复用
- ✅ 注意闭包问题和依赖数组遗漏


