React Hook 入门指南
适合新手学习,从零开始理解 React Hook,每个 Hook 都有详细的使用示例
一、什么是 Hook?
Hook 是 React 16.8 引入的一种机制,让你在函数组件中使用状态(state)和其他 React 特性,而无需编写类组件。
为什么需要 Hook?
在 Hook 出现之前,React 组件主要有两种写法:
// ❌ 类组件 - 代码冗长,this 指向容易出错
class Counter extends React.Component {
state = { count: 0 }
increment = () => {
this.setState({ count: this.state.count + 1 })
}
render() {
return (
<button onClick={this.increment}>
Count: {this.state.count}
</button>
)
}
}
// ✅ 函数组件 + Hook - 简洁直观
function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
)
}
函数组件的优势:
- 代码量减少 20-30%
- 没有
this指向问题 - 逻辑复用更简单
- 更容易理解和测试
二、内置 Hook 一览
React 提供了多个内置 Hook,按用途分类:
2.1 基础 Hook
| Hook | 用途 | 示例 |
|---|---|---|
useState | 管理组件状态 | const [count, setCount] = useState(0) |
useEffect | 处理副作用 | 数据获取、订阅、DOM 操作 |
useContext | 访问上下文 | 跨组件共享数据 |
2.2 性能优化 Hook
| Hook | 用途 | 示例 |
|---|---|---|
useMemo | 缓存计算结果 | 避免重复计算 |
useCallback | 缓存函数引用 | 避免子组件不必要的渲染 |
useRef | 获取 DOM 引用或保存可变值 | 访问 DOM 元素 |
2.3 其他 Hook
| Hook | 用途 |
|---|---|
useReducer | 复杂状态管理(类似 Redux) |
useLayoutEffect | 同步执行副作用(DOM 更新前) |
useImperativeHandle | 自定义暴露给父组件的方法 |
三、useState 详解
useState 是最基础的 Hook,用于在函数组件中添加状态。
3.1 基础用法:简单计数器
import { useState } from 'react'
function Counter() {
// 声明一个名为 count 的状态变量,初始值为 0
const [count, setCount] = useState(0)
return (
<div>
<p>当前计数: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
<button onClick={() => setCount(0)}>重置</button>
</div>
)
}
3.2 管理对象状态
import { useState } from 'react'
function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
})
// 更新单个字段
const updateField = (field: string, value: string | number) => {
setUser(prevUser => ({
...prevUser, // 保留其他字段
[field]: value
}))
}
return (
<form>
<div>
<label>姓名:</label>
<input
value={user.name}
onChange={e => updateField('name', e.target.value)}
/>
</div>
<div>
<label>邮箱:</label>
<input
value={user.email}
onChange={e => updateField('email', e.target.value)}
/>
</div>
<div>
<label>年龄:</label>
<input
type="number"
value={user.age}
onChange={e => updateField('age', parseInt(e.target.value) || 0)}
/>
</div>
<p>当前用户: {JSON.stringify(user)}</p>
</form>
)
}
3.3 管理数组状态
import { useState } from 'react'
function TodoList() {
const [todos, setTodos] = useState<string[]>([])
const [input, setInput] = useState('')
// 添加元素
const addTodo = () => {
if (input.trim()) {
setTodos([...todos, input.trim()])
setInput('')
}
}
// 删除元素
const removeTodo = (index: number) => {
setTodos(todos.filter((_, i) => i !== index))
}
// 清空所有
const clearAll = () => {
setTodos([])
}
return (
<div>
<div>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="输入待办事项"
/>
<button onClick={addTodo}>添加</button>
<button onClick={clearAll}>清空</button>
</div>
<ul>
{todos.map((todo, index) => (
<li key={index}>
{todo}
<button onClick={() => removeTodo(index)}>删除</button>
</li>
))}
</ul>
<p>共 {todos.length} 个待办事项</p>
</div>
)
}
3.4 函数式更新
当你需要基于前一个状态值来更新状态时,使用函数式更新:
import { useState } from 'react'
function AdvancedCounter() {
const [count, setCount] = useState(0)
// ✅ 推荐:函数式更新,确保拿到最新值
const increment = () => {
setCount(prev => prev + 1)
}
// 批量更新:连续增加 3 次
const incrementThreeTimes = () => {
// ❌ 错误方式:这样只会增加 1,因为 count 值在当前渲染周期不变
// setCount(count + 1)
// setCount(count + 1)
// setCount(count + 1)
// ✅ 正确方式:每次都基于最新的 prev 值
setCount(prev => prev + 1)
setCount(prev => prev + 1)
setCount(prev => prev + 1)
}
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={incrementThreeTimes}>+3</button>
</div>
)
}
3.5 惰性初始化
当初始值需要复杂计算时,可以传入一个函数,它只会在首次渲染时执行:
import { useState } from 'react'
function ExpensiveInitialValue() {
// ❌ 每次渲染都会执行这个计算
// const [state, setState] = useState(computeExpensiveValue())
// ✅ 惰性初始化:函数只在首次渲染时执行
const [state, setState] = useState(() => {
console.log('只在首次渲染时计算')
// 模拟昂贵的计算
let result = 0
for (let i = 0; i < 10000; i++) {
result += i
}
return result
})
return (
<div>
<p>初始值: {state}</p>
<button onClick={() => setState(s => s + 1)}>增加</button>
</div>
)
}
3.6 切换布尔值
import { useState } from 'react'
function ToggleButton() {
const [isOn, setIsOn] = useState(false)
const toggle = () => {
setIsOn(prev => !prev) // 切换布尔值
}
return (
<button onClick={toggle}>
状态: {isOn ? '开启' : '关闭'}
</button>
)
}
// 模态框示例
function Modal() {
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>打开模态框</button>
{isOpen && (
<div className="modal">
<div className="modal-content">
<h2>模态框标题</h2>
<p>这是模态框内容</p>
<button onClick={() => setIsOpen(false)}>关闭</button>
</div>
</div>
)}
</div>
)
}
四、useEffect 详解
useEffect 用于处理"副作用",比如数据获取、订阅、DOM 操作等。
4.1 基础用法:数据获取
import { useState, useEffect } from 'react'
interface User {
id: number
name: string
email: string
}
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
// 开始加载
setLoading(true)
setError(null)
// 请求用户数据
fetch(`/api/users/${userId}`)
.then(res => {
if (!res.ok) {
throw new Error('请求失败')
}
return res.json()
})
.then(data => {
setUser(data)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}, [userId]) // userId 变化时重新请求
if (loading) return <div>加载中...</div>
if (error) return <div>错误: {error}</div>
return (
<div>
<h2>{user?.name}</h2>
<p>邮箱: {user?.email}</p>
</div>
)
}
4.2 订阅与清理
import { useState, useEffect } from 'react'
function ChatRoom({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<string[]>([])
useEffect(() => {
// 模拟连接到聊天室
console.log(`连接到聊天室: ${roomId}`)
// 模拟接收消息
const interval = setInterval(() => {
setMessages(prev => [...prev, `新消息 @ ${new Date().toLocaleTimeString()}`])
}, 3000)
// 清理函数:组件卸载或 roomId 变化前执行
return () => {
console.log(`断开聊天室: ${roomId}`)
clearInterval(interval)
}
}, [roomId])
return (
<div>
<h3>聊天室: {roomId}</h3>
<ul>
{messages.map((msg, i) => (
<li key={i}>{msg}</li>
))}
</ul>
</div>
)
}
4.3 定时器示例
import { useState, useEffect } from 'react'
function Timer() {
const [seconds, setSeconds] = useState(0)
const [isRunning, setIsRunning] = useState(false)
useEffect(() => {
let interval: number | null = null
if (isRunning) {
interval = setInterval(() => {
setSeconds(prev => prev + 1)
}, 1000)
}
// 清理定时器
return () => {
if (interval) {
clearInterval(interval)
}
}
}, [isRunning])
const reset = () => {
setSeconds(0)
setIsRunning(false)
}
return (
<div>
<h2>计时器: {seconds}秒</h2>
<button onClick={() => setIsRunning(!isRunning)}>
{isRunning ? '暂停' : '开始'}
</button>
<button onClick={reset}>重置</button>
</div>
)
}
4.4 事件监听
import { useState, useEffect } from 'react'
function WindowSize() {
const [windowSize, setWindowSize] = useState({
width: typeof window !== 'undefined' ? window.innerWidth : 0,
height: typeof window !== 'undefined' ? window.innerHeight : 0
})
useEffect(() => {
// 定义处理函数
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
})
}
// 添加事件监听
window.addEventListener('resize', handleResize)
// 清理:移除事件监听
return () => {
window.removeEventListener('resize', handleResize)
}
}, []) // 空数组:只在挂载时添加,卸载时移除
return (
<div>
<p>窗口宽度: {windowSize.width}px</p>
<p>窗口高度: {windowSize.height}px</p>
</div>
)
}
// 键盘事件示例
function KeyboardListener() {
const [lastKey, setLastKey] = useState('')
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
setLastKey(e.key)
}
window.addEventListener('keydown', handleKeyDown)
return () => {
window.removeEventListener('keydown', handleKeyDown)
}
}, [])
return <p>你按下了: {lastKey || '等待输入...'}</p>
}
4.5 依赖数组的三种情况
import { useState, useEffect } from 'react'
function DependencyExamples({ userId, count }: { userId: string; count: number }) {
const [logs, setLogs] = useState<string[]>([])
// 情况1:无依赖数组 - 每次渲染后都执行
useEffect(() => {
console.log('每次渲染都执行')
})
// 情况2:空数组 - 只在挂载时执行一次
useEffect(() => {
console.log('组件挂载时执行一次')
// 相当于类组件的 componentDidMount
return () => {
console.log('组件卸载时执行')
// 相当于类组件的 componentWillUnmount
}
}, [])
// 情况3:有依赖 - 依赖变化时执行
useEffect(() => {
console.log(`userId 变化为: ${userId}`)
// 相当于类组件的 componentDidUpdate(针对特定 props)
}, [userId])
// 多个依赖
useEffect(() => {
console.log(`userId 或 count 变化: userId=${userId}, count=${count}`)
}, [userId, count])
return <div>查看控制台日志</div>
}
4.6 DOM 操作示例
import { useState, useEffect, useRef } from 'react'
function DynamicTitle() {
const [count, setCount] = useState(0)
// 更新页面标题
useEffect(() => {
document.title = `点击次数: ${count}`
}, [count])
return (
<button onClick={() => setCount(c => c + 1)}>
点击次数: {count}
</button>
)
}
// 自动滚动到底部
function ChatMessages({ messages }: { messages: string[] }) {
const bottomRef = useRef<HTMLDivElement>(null)
useEffect(() => {
// 新消息时滚动到底部
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])
return (
<div style={{ height: '200px', overflow: 'auto' }}>
{messages.map((msg, i) => (
<div key={i}>{msg}</div>
))}
<div ref={bottomRef} />
</div>
)
}
五、useRef 详解
useRef 有两个主要用途:
- 获取 DOM 元素引用
- 保存可变值(变化时不触发重新渲染)
5.1 基础用法:获取 DOM 引用
import { useRef, useEffect } from 'react'
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null)
const focusInput = () => {
// 访问 DOM 元素
inputRef.current?.focus()
}
useEffect(() => {
// 组件挂载后自动聚焦
inputRef.current?.focus()
}, [])
return (
<div>
<input ref={inputRef} type="text" placeholder="我会自动聚焦" />
<button onClick={focusInput}>手动聚焦</button>
</div>
)
}
5.2 表单提交示例
import { useRef } from 'react'
function FormWithRef() {
const nameRef = useRef<HTMLInputElement>(null)
const emailRef = useRef<HTMLInputElement>(null)
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// 直接获取表单值,不需要 useState
console.log({
name: nameRef.current?.value,
email: emailRef.current?.value
})
}
return (
<form onSubmit={handleSubmit}>
<div>
<label>姓名:</label>
<input ref={nameRef} type="text" defaultValue="" />
</div>
<div>
<label>邮箱:</label>
<input ref={emailRef} type="email" defaultValue="" />
</div>
<button type="submit">提交</button>
</form>
)
}
5.3 保存定时器 ID
import { useState, useRef, useEffect } from 'react'
function Stopwatch() {
const [time, setTime] = useState(0)
const [isRunning, setIsRunning] = useState(false)
const intervalRef = useRef<number | null>(null)
const start = () => {
if (!isRunning) {
setIsRunning(true)
intervalRef.current = setInterval(() => {
setTime(prev => prev + 1)
}, 1000)
}
}
const stop = () => {
setIsRunning(false)
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}
const reset = () => {
stop()
setTime(0)
}
// 组件卸载时清理
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
}
}
}, [])
return (
<div>
<h2>秒表: {time}秒</h2>
<button onClick={start} disabled={isRunning}>开始</button>
<button onClick={stop} disabled={!isRunning}>停止</button>
<button onClick={reset}>重置</button>
</div>
)
}
5.4 跟踪前一次值
import { useState, useRef, useEffect } from 'react'
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
// 使用示例
function CounterWithPrevious() {
const [count, setCount] = useState(0)
const prevCount = usePrevious(count)
return (
<div>
<p>当前值: {count}</p>
<p>前一个值: {prevCount ?? '无'}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
)
}
5.5 可变值(不触发渲染)
import { useState, useRef } from 'react'
function RenderCounter() {
const [count, setCount] = useState(0)
const renderCount = useRef(0)
// 每次渲染时增加,但不会触发额外的渲染
renderCount.current += 1
return (
<div>
<p>Count: {count}</p>
<p>组件渲染次数: {renderCount.current}</p>
<button onClick={() => setCount(c => c + 1)}>增加 Count</button>
</div>
)
}
5.6 useState vs useRef 对比
import { useState, useRef } from 'react'
function CompareStateAndRef() {
const [stateCount, setStateCount] = useState(0)
const refCount = useRef(0)
const incrementState = () => {
setStateCount(prev => prev + 1)
console.log('State 更新,会触发重新渲染')
}
const incrementRef = () => {
refCount.current += 1
console.log('Ref 更新,不会触发重新渲染')
console.log('当前 ref 值:', refCount.current)
}
return (
<div>
<div>
<h3>useState</h3>
<p>值: {stateCount} (UI 会更新)</p>
<button onClick={incrementState}>增加 State</button>
</div>
<div>
<h3>useRef</h3>
<p>值: {refCount.current} (UI 不会自动更新)</p>
<button onClick={incrementRef}>增加 Ref</button>
<button onClick={() => setStateCount(c => c)}>强制刷新</button>
</div>
</div>
)
}
| 特性 | useState | useRef |
|---|---|---|
| 值变化时重新渲染 | ✅ 是 | ❌ 否 |
| 用途 | 需要显示的数据 | DOM 引用、定时器 ID、前一次值等 |
| 更新方式 | setter 函数 | 直接修改 .current |
六、useContext 详解
useContext 用于跨组件共享数据,避免 prop drilling(层层传递 props)。
6.1 基础用法:创建和使用 Context
import { createContext, useContext, useState } from 'react'
// 1. 创建 Context
interface ThemeContextType {
theme: 'light' | 'dark'
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
// 2. 创建 Provider 组件
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// 3. 创建自定义 Hook 方便使用
function useTheme() {
const context = useContext(ThemeContext)
if (context === undefined) {
throw new Error('useTheme 必须在 ThemeProvider 内使用')
}
return context
}
// 4. 使用 Context 的组件
function ThemedButton() {
const { theme, toggleTheme } = useTheme()
return (
<button
onClick={toggleTheme}
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff',
border: '1px solid',
padding: '10px 20px'
}}
>
当前主题: {theme}
</button>
)
}
function ThemedBox() {
const { theme } = useTheme()
return (
<div style={{
background: theme === 'light' ? '#f5f5f5' : '#222',
color: theme === 'light' ? '#333' : '#fff',
padding: '20px'
}}>
这是一个主题化的盒子
</div>
)
}
// 5. 应用入口
function App() {
return (
<ThemeProvider>
<div>
<h1>主题切换示例</h1>
<ThemedButton />
<ThemedBox />
</div>
</ThemeProvider>
)
}
export default App
6.2 用户认证 Context
import { createContext, useContext, useState, ReactNode } from 'react'
interface User {
id: string
name: string
email: string
}
interface AuthContextType {
user: User | null
login: (email: string, password: string) => Promise<void>
logout: () => void
isAuthenticated: boolean
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const login = async (email: string, password: string) => {
// 模拟登录请求
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
})
const userData = await response.json()
setUser(userData)
}
const logout = () => {
setUser(null)
}
return (
<AuthContext.Provider value={{
user,
login,
logout,
isAuthenticated: !!user
}}>
{children}
</AuthContext.Provider>
)
}
function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth 必须在 AuthProvider 内使用')
}
return context
}
// 使用示例
function LoginButton() {
const { user, login, logout, isAuthenticated } = useAuth()
if (isAuthenticated) {
return (
<div>
<span>欢迎, {user?.name}</span>
<button onClick={logout}>退出登录</button>
</div>
)
}
return (
<button onClick={() => login('test@example.com', 'password')}>
登录
</button>
)
}
function ProtectedContent() {
const { isAuthenticated } = useAuth()
if (!isAuthenticated) {
return <p>请先登录查看内容</p>
}
return <p>这是受保护的内容,只有登录用户可见</p>
}
6.3 多层嵌套 Context
import { createContext, useContext, useState } from 'react'
// Context 1: 用户信息
const UserContext = createContext<{ name: string } | null>(null)
// Context 2: 主题设置
const ThemeContext = createContext<{ theme: string } | null>(null)
// Context 3: 语言设置
const LocaleContext = createContext<{ locale: string } | null>(null)
function DeepNestedComponent() {
// 可以同时使用多个 Context
const user = useContext(UserContext)
const theme = useContext(ThemeContext)
const locale = useContext(LocaleContext)
return (
<div>
<p>用户: {user?.name}</p>
<p>主题: {theme?.theme}</p>
<p>语言: {locale?.locale}</p>
</div>
)
}
function App() {
return (
<UserContext.Provider value={{ name: '张三' }}>
<ThemeContext.Provider value={{ theme: 'dark' }}>
<LocaleContext.Provider value={{ locale: 'zh-CN' }}>
<DeepNestedComponent />
</LocaleContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
)
}
七、useMemo 详解
useMemo 用于缓存计算结果,避免在每次渲染时重复执行昂贵的计算。
7.1 基础用法:缓存计算结果
import { useState, useMemo } from 'react'
function ExpensiveCalculation({ numbers }: { numbers: number[] }) {
// 昂贵的计算函数
const calculateSum = (nums: number[]): number => {
console.log('执行计算...') // 观察是否执行
let sum = 0
for (let i = 0; i < 10000000; i++) {
// 模拟耗时计算
sum += nums.reduce((a, b) => a + b, 0)
}
return sum
}
// ❌ 不使用 useMemo:每次渲染都会重新计算
// const total = calculateSum(numbers)
// ✅ 使用 useMemo:只在 numbers 变化时重新计算
const total = useMemo(() => {
return calculateSum(numbers)
}, [numbers])
return <div>计算结果: {total}</div>
}
function App() {
const [numbers] = useState([1, 2, 3, 4, 5])
const [count, setCount] = useState(0) // 无关的状态
return (
<div>
<ExpensiveCalculation numbers={numbers} />
<button onClick={() => setCount(c => c + 1)}>
点击计数: {count}
</button>
<p>点击按钮不会触发重新计算,因为 numbers 没变</p>
</div>
)
}
7.2 列表过滤和排序
import { useState, useMemo } from 'react'
interface Product {
id: number
name: string
price: number
category: string
}
function ProductList({ products }: { products: Product[] }) {
const [searchTerm, setSearchTerm] = useState('')
const [sortBy, setSortBy] = useState<'name' | 'price'>('name')
const [category, setCategory] = useState<string>('all')
// 过滤和排序:只在相关状态变化时重新计算
const filteredProducts = useMemo(() => {
console.log('重新计算过滤结果')
let result = [...products]
// 按类别过滤
if (category !== 'all') {
result = result.filter(p => p.category === category)
}
// 按搜索词过滤
if (searchTerm) {
result = result.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
)
}
// 排序
result.sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name)
}
return a.price - b.price
})
return result
}, [products, searchTerm, sortBy, category])
return (
<div>
<div>
<input
placeholder="搜索..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
<select value={category} onChange={e => setCategory(e.target.value)}>
<option value="all">全部类别</option>
<option value="electronics">电子产品</option>
<option value="clothing">服装</option>
</select>
<select value={sortBy} onChange={e => setSortBy(e.target.value as 'name' | 'price')}>
<option value="name">按名称</option>
<option value="price">按价格</option>
</select>
</div>
<ul>
{filteredProducts.map(product => (
<li key={product.id}>
{product.name} - ¥{product.price}
</li>
))}
</ul>
</div>
)
}
7.3 避免子组件不必要的渲染
import { useState, useMemo, memo } from 'react'
// 子组件使用 memo 包裹
const ExpensiveChild = memo(function ExpensiveChild({
data
}: {
data: { items: number[] }
}) {
console.log('子组件渲染')
return (
<div>
数据项数量: {data.items.length}
</div>
)
})
function ParentComponent() {
const [count, setCount] = useState(0)
const [items] = useState([1, 2, 3, 4, 5])
// ✅ 使用 useMemo:保持对象引用稳定
const data = useMemo(() => ({
items
}), [items])
// ❌ 不使用 useMemo:每次渲染都创建新对象
// const data = { items }
// 这样会导致 ExpensiveChild 每次都重新渲染
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
计数: {count}
</button>
<ExpensiveChild data={data} />
<p>点击按钮,子组件不会重新渲染</p>
</div>
)
}
7.4 计算派生状态
import { useState, useMemo } from 'react'
interface Todo {
id: number
text: string
completed: boolean
}
function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, text: '学习 React', completed: true },
{ id: 2, text: '学习 TypeScript', completed: false },
{ id: 3, text: '写项目', completed: false }
])
const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all')
// 派生状态:根据过滤条件计算显示的 todo 列表
const visibleTodos = useMemo(() => {
switch (filter) {
case 'active':
return todos.filter(t => !t.completed)
case 'completed':
return todos.filter(t => t.completed)
default:
return todos
}
}, [todos, filter])
// 统计信息
const stats = useMemo(() => ({
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length
}), [todos])
return (
<div>
<div>
<button onClick={() => setFilter('all')}>全部 ({stats.total})</button>
<button onClick={() => setFilter('active')}>待办 ({stats.active})</button>
<button onClick={() => setFilter('completed')}>已完成 ({stats.completed})</button>
</div>
<ul>
{visibleTodos.map(todo => (
<li key={todo.id}>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
</li>
))}
</ul>
</div>
)
}
八、useCallback 详解
useCallback 用于缓存函数引用,避免在每次渲染时创建新的函数实例。
8.1 基础用法:缓存回调函数
import { useState, useCallback } from 'react'
function SearchComponent() {
const [query, setQuery] = useState('')
const [results, setResults] = useState<string[]>([])
// ✅ 使用 useCallback:函数引用稳定
const handleSearch = useCallback((searchQuery: string) => {
console.log('搜索:', searchQuery)
// 模拟搜索
setResults(['结果1', '结果2', '结果3'])
}, []) // 无依赖,函数永远不会改变
// ❌ 不使用 useCallback:每次渲染都创建新函数
// const handleSearch = (searchQuery: string) => {
// console.log('搜索:', searchQuery)
// }
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
/>
<button onClick={() => handleSearch(query)}>搜索</button>
<ul>
{results.map((r, i) => <li key={i}>{r}</li>)}
</ul>
</div>
)
}
8.2 配合子组件使用
import { useState, useCallback, memo } from 'react'
// 子组件:使用 memo 包裹,只有 props 变化才重新渲染
const Button = memo(function Button({
onClick,
label
}: {
onClick: () => void
label: string
}) {
console.log(`Button "${label}" 渲染`)
return <button onClick={onClick}>{label}</button>
})
function ParentWithCallback() {
const [countA, setCountA] = useState(0)
const [countB, setCountB] = useState(0)
// ✅ 使用 useCallback:保持函数引用稳定
const incrementA = useCallback(() => {
setCountA(c => c + 1)
}, [])
const incrementB = useCallback(() => {
setCountB(c => c + 1)
}, [])
// ❌ 不使用 useCallback:每次渲染都会创建新函数
// 这样 Button 组件每次都会重新渲染
// const incrementA = () => setCountA(c => c + 1)
// const incrementB = () => setCountB(c => c + 1)
return (
<div>
<p>Count A: {countA}</p>
<p>Count B: {countB}</p>
<Button onClick={incrementA} label="增加 A" />
<Button onClick={incrementB} label="增加 B" />
</div>
)
}
8.3 依赖其他状态
import { useState, useCallback } from 'react'
function TodoList() {
const [todos, setTodos] = useState<string[]>([])
const [input, setInput] = useState('')
// ✅ 使用 useCallback + 依赖
const addTodo = useCallback(() => {
if (input.trim()) {
setTodos(prev => [...prev, input.trim()])
setInput('')
}
}, [input]) // input 变化时,函数会更新
// 删除 todo
const removeTodo = useCallback((index: number) => {
setTodos(prev => prev.filter((_, i) => i !== index))
}, []) // 不依赖外部变量,函数稳定
return (
<div>
<input
value={input}
onChange={e => setInput(e.target.value)}
/>
<button onClick={addTodo}>添加</button>
<ul>
{todos.map((todo, index) => (
<li key={index}>
{todo}
<button onClick={() => removeTodo(index)}>删除</button>
</li>
))}
</ul>
</div>
)
}
8.4 传递给 useEffect 依赖
import { useState, useCallback, useEffect } from 'react'
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<{ name: string } | null>(null)
// ✅ 使用 useCallback 创建稳定的函数引用
const fetchUser = useCallback(async () => {
const response = await fetch(`/api/users/${userId}`)
const data = await response.json()
setUser(data)
}, [userId]) // userId 变化时函数会更新
// 可以安全地放入依赖数组
useEffect(() => {
fetchUser()
}, [fetchUser])
return <div>{user?.name ?? '加载中...'}</div>
}
8.5 useCallback vs useMemo
import { useCallback, useMemo } from 'react'
function CallbackVsMemo() {
const [count, setCount] = useState(0)
// useCallback: 缓存函数本身
const handleClick = useCallback(() => {
console.log('点击')
}, [])
// useMemo: 缓存函数的返回值(虽然也可以缓存函数)
const memoizedFunction = useMemo(() => {
return () => console.log('点击')
}, [])
// 实际上 useCallback(fn, deps) 等价于 useMemo(() => fn, deps)
return <button onClick={handleClick}>Count: {count}</button>
}
| 特性 | useCallback | useMemo |
|---|---|---|
| 缓存内容 | 函数引用 | 任意计算结果 |
| 返回值 | 函数本身 | 计算结果 |
| 使用场景 | 传递给子组件的回调、useEffect 依赖 | 昂贵的计算、派生状态 |
九、useReducer 详解
useReducer 用于管理复杂状态逻辑,是 useState 的替代方案,适合状态更新逻辑复杂的场景。
9.1 基础用法:计数器
import { useReducer } from 'react'
// 1. 定义状态类型
interface State {
count: number
}
// 2. 定义 action 类型
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset' }
| { type: 'set'; payload: number }
// 3. 定义 reducer 函数
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
case 'reset':
return { count: 0 }
case 'set':
return { count: action.payload }
default:
throw new Error('Unknown action')
}
}
function Counter() {
// useReducer(reducer, initial state)
const [state, dispatch] = useReducer(reucer, { count: 0 })
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'reset' })}>重置</button>
<button onClick={() => dispatch({ type: 'set', payload: 10 })}>
设为 10
</button>
</div>
)
}
9.2 复杂表单状态
import { useReducer } from 'react'
interface FormState {
username: string
email: string
password: string
errors: {
username?: string
email?: string
password?: string
}
isSubmitting: boolean
}
type FormAction =
| { type: 'SET_FIELD'; field: keyof Omit<FormState, 'errors' | 'isSubmitting'>; value: string }
| { type: 'SET_ERROR'; field: keyof FormState['errors']; error: string | undefined }
| { type: 'CLEAR_ERRORS' }
| { type: 'START_SUBMIT' }
| { type: 'END_SUBMIT' }
| { type: 'RESET' }
const initialState: FormState = {
username: '',
email: '',
password: '',
errors: {},
isSubmitting: false
}
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'SET_FIELD':
return { ...state, [action.field]: action.value }
case 'SET_ERROR':
return {
...state,
errors: { ...state.errors, [action.field]: action.error }
}
case 'CLEAR_ERRORS':
return { ...state, errors: {} }
case 'START_SUBMIT':
return { ...state, isSubmitting: true }
case 'END_SUBMIT':
return { ...state, isSubmitting: false }
case 'RESET':
return initialState
default:
return state
}
}
function ComplexForm() {
const [state, dispatch] = useReducer(formReducer, initialState)
const handleChange = (field: keyof Omit<FormState, 'errors' | 'isSubmitting'>) => (
e: React.ChangeEvent<HTMLInputElement>
) => {
dispatch({ type: 'SET_FIELD', field, value: e.target.value })
// 清除该字段的错误
dispatch({ type: 'SET_ERROR', field: field as keyof FormState['errors'], error: undefined })
}
const validate = () => {
let hasError = false
if (!state.username) {
dispatch({ type: 'SET_ERROR', field: 'username', error: '用户名必填' })
hasError = true
}
if (!state.email.includes('@')) {
dispatch({ type: 'SET_ERROR', field: 'email', error: '邮箱格式不正确' })
hasError = true
}
if (state.password.length < 6) {
dispatch({ type: 'SET_ERROR', field: 'password', error: '密码至少6位' })
hasError = true
}
return !hasError
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!validate()) return
dispatch({ type: 'START_SUBMIT' })
// 模拟提交
await new Promise(resolve => setTimeout(resolve, 1000))
dispatch({ type: 'END_SUBMIT' })
dispatch({ type: 'RESET' })
}
return (
<form onSubmit={handleSubmit}>
<div>
<input
value={state.username}
onChange={handleChange('username')}
placeholder="用户名"
/>
{state.errors.username && <span>{state.errors.username}</span>}
</div>
<div>
<input
value={state.email}
onChange={handleChange('email')}
placeholder="邮箱"
/>
{state.errors.email && <span>{state.errors.email}</span>}
</div>
<div>
<input
type="password"
value={state.password}
onChange={handleChange('password')}
placeholder="密码"
/>
{state.errors.password && <span>{state.errors.password}</span>}
</div>
<button type="submit" disabled={state.isSubmitting}>
{state.isSubmitting ? '提交中...' : '提交'}
</button>
<button type="button" onClick={() => dispatch({ type: 'RESET' })}>
重置
</button>
</form>
)
}
9.3 Todo 列表(经典示例)
import { useReducer } from 'react'
interface Todo {
id: number
text: string
completed: boolean
}
type TodoAction =
| { type: 'add'; text: string }
| { type: 'toggle'; id: number }
| { type: 'delete'; id: number }
| { type: 'clear_completed' }
function todosReducer(todos: Todo[], action: TodoAction): Todo[] {
switch (action.type) {
case 'add':
return [...todos, {
id: Date.now(),
text: action.text,
completed: false
}]
case 'toggle':
return todos.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
)
case 'delete':
return todos.filter(todo => todo.id !== action.id)
case 'clear_completed':
return todos.filter(todo => !todo.completed)
default:
return todos
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(todosReducer, [])
const [input, setInput] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (input.trim()) {
dispatch({ type: 'add', text: input.trim() })
setInput('')
}
}
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="添加待办事项"
/>
<button type="submit">添加</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch({ type: 'toggle', id: todo.id })}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'delete', id: todo.id })}>
删除
</button>
</li>
))}
</ul>
<button onClick={() => dispatch({ type: 'clear_completed' })}>
清除已完成
</button>
</div>
)
}
9.4 useReducer vs useState
| 特性 | useState | useReducer |
|---|---|---|
| 适用场景 | 简单状态(单个值) | 复杂状态(多字段、多逻辑) |
| 更新方式 | 直接设置值 | 通过 action 描述更新 |
| 可测试性 | 较难单独测试 | reducer 是纯函数,易测试 |
| 状态逻辑 | 分散在组件中 | 集中在 reducer 中 |
// useState:适合简单状态
const [count, setCount] = useState(0)
// useReducer:适合复杂状态
const [state, dispatch] = useReducer(reducer, initialState)
十、useLayoutEffect 详解
useLayoutEffect 与 useEffect 类似,但它在所有 DOM 变更后同步触发,在浏览器绘制之前执行。
10.1 基础用法:防止闪烁
import { useState, useLayoutEffect, useRef } from 'react'
function Tooltip({ content }: { content: string }) {
const [position, setPosition] = useState({ x: 0, y: 0 })
const [visible, setVisible] = useState(false)
const tooltipRef = useRef<HTMLDivElement>(null)
useLayoutEffect(() => {
if (visible && tooltipRef.current) {
// 在绘制前计算位置,避免闪烁
const rect = tooltipRef.current.getBoundingClientRect()
// 如果超出屏幕,调整位置
if (rect.right > window.innerWidth) {
setPosition(prev => ({
...prev,
x: prev.x - rect.width
}))
}
}
}, [visible])
return (
<div>
<button
onMouseEnter={() => setVisible(true)}
onMouseLeave={() => setVisible(false)}
>
悬停显示提示
</button>
{visible && (
<div
ref={tooltipRef}
style={{
position: 'absolute',
left: position.x,
top: position.y,
background: '#333',
color: '#fff',
padding: '5px 10px'
}}
>
{content}
</div>
)}
</div>
)
}
10.2 动画初始状态
import { useState, useLayoutEffect, useRef } from 'react'
function AnimatedModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
const modalRef = useRef<HTMLDivElement>(null)
useLayoutEffect(() => {
if (isOpen && modalRef.current) {
// 在绘制前设置初始状态
modalRef.current.style.opacity = '0'
modalRef.current.style.transform = 'scale(0.9)'
// 强制浏览器应用初始状态后开始动画
requestAnimationFrame(() => {
if (modalRef.current) {
modalRef.current.style.opacity = '1'
modalRef.current.style.transform = 'scale(1)'
}
})
}
}, [isOpen])
if (!isOpen) return null
return (
<div className="modal-overlay" onClick={onClose}>
<div
ref={modalRef}
className="modal-content"
style={{
transition: 'opacity 0.3s, transform 0.3s'
}}
onClick={e => e.stopPropagation()}
>
<h2>模态框标题</h2>
<p>模态框内容</p>
<button onClick={onClose}>关闭</button>
</div>
</div>
)
}
10.3 useEffect vs useLayoutEffect
import { useState, useEffect, useLayoutEffect, useRef } from 'react'
function EffectComparison() {
const [count, setCount] = useState(0)
const boxRef = useRef<HTMLDivElement>(null)
// useEffect:浏览器绘制后执行
useEffect(() => {
console.log('useEffect: DOM 已绘制')
})
// useLayoutEffect:浏览器绘制前执行
useLayoutEffect(() => {
console.log('useLayoutEffect: DOM 更新后,绘制前')
if (boxRef.current) {
// 同步修改 DOM,不会看到闪烁
boxRef.current.style.background = count % 2 === 0 ? 'lightblue' : 'lightgreen'
}
})
return (
<div>
<div
ref={boxRef}
style={{
width: '100px',
height: '100px',
transition: 'background 0.3s'
}}
>
Count: {count}
</div>
<button onClick={() => setCount(c => c + 1)}>增加</button>
</div>
)
}
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | DOM 更新后,浏览器绘制后 | DOM 更新后,浏览器绘制前 |
| 阻塞绘制 | 否(异步) | 是(同步) |
| 使用场景 | 大多数副作用 | DOM 测量、防止闪烁 |
| 服务端渲染 | 支持 | 会警告,需特殊处理 |
十一、useImperativeHandle 详解
useImperativeHandle 用于自定义暴露给父组件的实例方法,需要配合 forwardRef 使用。
11.1 基础用法
import { useRef, useImperativeHandle, forwardRef } from 'react'
// 子组件
interface InputHandle {
focus: () => void
clear: () => void
getValue: () => string
}
const CustomInput = forwardRef<InputHandle, { defaultValue?: string }>(
({ defaultValue = '' }, ref) => {
const inputRef = useRef<HTMLInputElement>(null)
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus()
},
clear: () => {
if (inputRef.current) {
inputRef.current.value = ''
}
},
getValue: () => {
return inputRef.current?.value ?? ''
}
}))
return (
<input
ref={inputRef}
type="text"
defaultValue={defaultValue}
style={{ padding: '8px', fontSize: '16px' }}
/>
)
}
)
// 父组件
function ParentComponent() {
const inputRef = useRef<InputHandle>(null)
const handleFocus = () => {
inputRef.current?.focus()
}
const handleClear = () => {
inputRef.current?.clear()
}
const handleGetValue = () => {
alert(`当前值: ${inputRef.current?.getValue()}`)
}
return (
<div>
<CustomInput ref={inputRef} defaultValue="初始值" />
<div style={{ marginTop: '10px' }}>
<button onClick={handleFocus}>聚焦</button>
<button onClick={handleClear}>清空</button>
<button onClick={handleGetValue}>获取值</button>
</div>
</div>
)
}
11.2 模态框组件示例
import { useState, useRef, useImperativeHandle, forwardRef } from 'react'
interface ModalHandle {
open: () => void
close: () => void
}
interface ModalProps {
title: string
children: React.ReactNode
onConfirm?: () => void
}
const Modal = forwardRef<ModalHandle, ModalProps>(
({ title, children, onConfirm }, ref) => {
const [isOpen, setIsOpen] = useState(false)
useImperativeHandle(ref, () => ({
open: () => setIsOpen(true),
close: () => setIsOpen(false)
}))
if (!isOpen) return null
return (
<div className="modal-overlay" onClick={() => setIsOpen(false)}>
<div
className="modal-content"
onClick={e => e.stopPropagation()}
style={{
background: 'white',
padding: '20px',
borderRadius: '8px',
maxWidth: '400px'
}}
>
<h2>{title}</h2>
<div>{children}</div>
<div style={{ marginTop: '20px' }}>
<button onClick={() => setIsOpen(false)}>取消</button>
<button
onClick={() => {
onConfirm?.()
setIsOpen(false)
}}
>
确认
</button>
</div>
</div>
</div>
)
}
)
// 使用示例
function App() {
const modalRef = useRef<ModalHandle>(null)
return (
<div>
<button onClick={() => modalRef.current?.open()}>
打开模态框
</button>
<Modal
ref={modalRef}
title="确认操作"
onConfirm={() => console.log('已确认')}
>
<p>你确定要执行此操作吗?</p>
</Modal>
</div>
)
}
11.3 暴露部分方法
import { useRef, useImperativeHandle, forwardRef, useState } from 'react'
interface VideoPlayerHandle {
play: () => void
pause: () => void
// 注意:不暴露其他内部方法
}
const VideoPlayer = forwardRef<VideoPlayerHandle, { src: string }>(
({ src }, ref) => {
const videoRef = useRef<HTMLVideoElement>(null)
const [isPlaying, setIsPlaying] = useState(false)
// 只暴露 play 和 pause 方法
useImperativeHandle(ref, () => ({
play: () => {
videoRef.current?.play()
setIsPlaying(true)
},
pause: () => {
videoRef.current?.pause()
setIsPlaying(false)
}
}))
// 内部方法,不暴露给父组件
const handleTimeUpdate = () => {
// 处理时间更新
}
const handleEnded = () => {
setIsPlaying(false)
}
return (
<div>
<video
ref={videoRef}
src={src}
onTimeUpdate={handleTimeUpdate}
onEnded={handleEnded}
/>
<p>状态: {isPlaying ? '播放中' : '已暂停'}</p>
</div>
)
}
)
十二、自定义 Hook
12.1 什么是自定义 Hook?
当多个组件需要共享相同逻辑时,可以抽取为自定义 Hook。
规则:
- 函数名必须以
use开头(如useFetchUser) - 内部可以调用其他 Hook
12.2 示例:封装数据获取逻辑
// hooks/useFetch.ts
import { useState, useEffect } from 'react'
interface UseFetchResult<T> {
data: T | null
loading: boolean
error: Error | null
refetch: () => void
}
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
const fetchData = async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const json = await response.json()
setData(json)
} catch (e) {
setError(e instanceof Error ? e : new Error('Unknown error'))
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchData()
}, [url])
return { data, loading, error, refetch: fetchData }
}
export default useFetch
// 使用自定义 Hook
function UserProfile({ userId }: { userId: string }) {
const { data: user, loading, error, refetch } = useFetch<User>(
`/api/users/${userId}`
)
if (loading) return <div>加载中...</div>
if (error) return <div>错误: {error.message}</div>
return (
<div>
<h2>{user?.name}</h2>
<p>邮箱: {user?.email}</p>
<button onClick={refetch}>刷新</button>
</div>
)
}
12.3 自定义 Hook:useLocalStorage
// hooks/useLocalStorage.ts
import { useState, useEffect } from 'react'
function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
// 初始化时从 localStorage 读取
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') {
return initialValue
}
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.error(error)
return initialValue
}
})
// 更新时同步到 localStorage
useEffect(() => {
if (typeof window !== 'undefined') {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue))
} catch (error) {
console.error(error)
}
}
}, [key, storedValue])
const setValue = (value: T | ((prev: T) => T)) => {
setStoredValue(prev => {
const newValue = value instanceof Function ? value(prev) : value
return newValue
})
}
return [storedValue, setValue]
}
export default useLocalStorage
// 使用示例
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
当前主题: {theme}
</button>
)
}
function PersistentForm() {
const [name, setName] = useLocalStorage('form-name', '')
const [email, setEmail] = useLocalStorage('form-email', '')
return (
<form>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="姓名"
/>
<input
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="邮箱"
/>
<p>刷新页面,数据不会丢失</p>
</form>
)
}
12.4 自定义 Hook:useDebounce
// hooks/useDebounce.ts
import { useState, useEffect } from 'react'
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(timer)
}
}, [value, delay])
return debouncedValue
}
export default useDebounce
// 使用示例:搜索输入
function SearchInput() {
const [searchTerm, setSearchTerm] = useState('')
const debouncedSearch = useDebounce(searchTerm, 500)
// 只在 debouncedSearch 变化时发起请求
useEffect(() => {
if (debouncedSearch) {
console.log('搜索:', debouncedSearch)
// fetchSearchResults(debouncedSearch)
}
}, [debouncedSearch])
return (
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="输入搜索关键词..."
/>
)
}
12.5 自定义 Hook:useWindowSize
// hooks/useWindowSize.ts
import { useState, useEffect } from 'react'
interface WindowSize {
width: number
height: number
}
function useWindowSize(): WindowSize {
const [windowSize, setWindowSize] = useState<WindowSize>({
width: typeof window !== 'undefined' ? window.innerWidth : 0,
height: typeof window !== 'undefined' ? window.innerHeight : 0
})
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
})
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return windowSize
}
export default useWindowSize
// 使用示例
function ResponsiveComponent() {
const { width, height } = useWindowSize()
return (
<div>
<p>窗口大小: {width} x {height}</p>
{width < 768 && <p>移动端布局</p>}
{width >= 768 && width < 1024 && <p>平板布局</p>}
{width >= 1024 && <p>桌面端布局</p>}
</div>
)
}
12.6 自定义 Hook 的独立性
每次调用都是独立的状态实例:
function Component() {
// 两个独立的请求,互不影响
const user1 = useFetch('/api/users/1')
const user2 = useFetch('/api/users/2')
return (
<div>
{user1.loading ? '加载中...' : user1.data?.name} -
{user2.loading ? '加载中...' : user2.data?.name}
</div>
)
}
十三、TanStack Query(推荐)
对于数据获取场景,推荐使用 TanStack Query(原 React Query),它提供了更强大的功能。
13.1 基本使用
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
// 查询数据
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) throw new Error('请求失败')
return response.json()
}
})
if (isLoading) return <Spinner />
if (error) return <ErrorMessage />
return (
<div>
<h2>{data.name}</h2>
<button onClick={() => refetch()}>刷新</button>
</div>
)
}
// 修改数据
function UpdateUser() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: async (newName: string) => {
const response = await fetch('/api/users/1', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName })
})
return response.json()
},
onSuccess: () => {
// 更新成功后,使缓存失效,重新获取数据
queryClient.invalidateQueries({ queryKey: ['user'] })
}
})
return (
<button onClick={() => mutation.mutate('新名字')}>
{mutation.isPending ? '更新中...' : '更新用户名'}
</button>
)
}
13.2 核心优势
| 特性 | 说明 |
|---|---|
| 自动缓存 | 相同 queryKey 的请求自动缓存 |
| 后台刷新 | 窗口聚焦时自动更新数据 |
| 重复请求合并 | 多个组件请求同一数据,只发一次请求 |
| 状态管理 | 内置 loading、error、refetch 等 |
| 失败重试 | 自动重试失败的请求 |
13.3 缓存机制
// 组件 A
const { data } = useQuery({
queryKey: ['user', '123'],
queryFn: fetchUser
})
// 组件 B(其他地方)
const { data } = useQuery({
queryKey: ['user', '123'],
queryFn: fetchUser
})
// ✅ 只发起一次网络请求,两个组件共享数据
十四、Hook 使用规则
React 对 Hook 有两条强制规则:
规则 1:只在顶层调用
// ❌ 错误:在条件语句中调用
if (condition) {
const [count, setCount] = useState(0)
}
// ❌ 错误:在循环中调用
for (let i = 0; i < 3; i++) {
const [value, setValue] = useState(i)
}
// ✅ 正确:始终在顶层调用
const [count, setCount] = useState(0)
if (condition) {
setCount(count + 1)
}
原因: React 依靠 Hook 的调用顺序来管理状态。
规则 2:只在 React 函数中调用
// ❌ 错误:在普通函数中调用
function handleClick() {
const [count, setCount] = useState(0) // 会报错!
}
// ✅ 正确:在组件或自定义 Hook 中调用
function MyComponent() {
const [count, setCount] = useState(0) // OK
}
// ✅ 正确:在自定义 Hook 中调用
function useMyHook() {
const [count, setCount] = useState(0) // OK
return count
}
ESLint 检查
推荐使用 eslint-plugin-react-hooks 插件,自动检查这些规则:
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
十五、常见问题
Q1: 为什么 useEffect 中的数据不是最新的?
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
console.log(count) // 始终打印 0!
}, 1000)
return () => clearInterval(id)
}, []) // 空依赖数组
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
原因: 空依赖数组导致 effect 只执行一次,闭包捕获的是初始值。
解决方案:
// 方案1:添加依赖
useEffect(() => {
const id = setInterval(() => {
console.log(count)
}, 1000)
return () => clearInterval(id)
}, [count]) // 添加 count 到依赖
// 方案2:使用函数式更新
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1) // 使用函数式更新
}, 1000)
return () => clearInterval(id)
}, [])
// 方案3:使用 useRef
const countRef = useRef(count)
countRef.current = count
useEffect(() => {
const id = setInterval(() => {
console.log(countRef.current)
}, 1000)
return () => clearInterval(id)
}, [])
Q2: 什么时候用 useMemo 和 useCallback?
function Parent({ items }: { items: Item[] }) {
// useMemo: 缓存计算结果
const sortedItems = useMemo(() => {
console.log('重新排序')
return [...items].sort((a, b) => a.name.localeCompare(b.name))
}, [items])
// useCallback: 缓存函数引用
const handleClick = useCallback((id: string) => {
console.log('Clicked:', id)
}, [])
return <Child items={sortedItems} onClick={handleClick} />
}
原则: 先写代码,遇到性能问题再优化。大多数情况下不需要过早优化。
Q3: 函数组件 vs 类组件,应该用哪个?
新项目:全部使用函数组件 + Hook
| 场景 | 推荐 |
|---|---|
| 新项目 | 函数组件 |
| 旧项目维护 | 可以混用,新代码用函数组件 |
| 错误边界 | 类组件(目前唯一例外) |
React 官方明确表示:Hook 是 React 的未来,新特性优先支持函数组件。
十六、命名约定
React 生态有统一的命名约定:
| 前缀 | 用途 | 示例 |
|---|---|---|
use | Hook | useState, useFetch, useGetUser |
handle | 事件处理函数 | handleClick, handleSubmit |
on | 事件回调 prop | onClick, onChange, onSubmit |
is/has | 布尔值 | isLoading, hasError, isVisible |
get | 获取数据的函数 | getUser, getParams |
Hook 命名示例:
// 内置 Hook
useState, useEffect, useContext, useRef
// 自定义 Hook
useFetch, useLocalStorage, useDebounce
// TanStack Query 封装
useGetWebAppParams, useGetUserCanAccessApp
十七、学习路径建议
-
基础阶段
- 理解
useState和useEffect - 完成官方教程的小项目
- 理解
-
进阶阶段
- 学习
useContext、useReducer - 学习自定义 Hook 的封装
- 了解性能优化(
useMemo、useCallback)
- 学习
-
实战阶段
- 学习 TanStack Query 处理数据请求
- 学习状态管理(Zustand、Jotai 等)
- 阅读优秀开源项目的代码
-
推荐资源
- React 官方文档
- TanStack Query 文档
- useHooks - 自定义 Hook 示例集合
十八、总结
| Hook | 用途 | 核心要点 |
|---|---|---|
useState | 状态管理 | 返回 [值, 更新函数],不可变更新 |
useEffect | 副作用处理 | 注意依赖数组,记得清理 |
useRef | DOM引用/持久值 | 修改 .current 不触发渲染 |
useContext | 跨组件共享 | 避免 prop drilling |
useMemo | 缓存计算结果 | 优化昂贵计算 |
useCallback | 缓存函数引用 | 配合 memo 子组件使用 |
useReducer | 复杂状态 | 类似 Redux 的模式 |
useLayoutEffect | 同步副作用 | DOM 测量、防闪烁 |
useImperativeHandle | 暴露方法 | 配合 forwardRef |
记住:Hook 让 React 更简单、更优雅。掌握 Hook,就是掌握了现代 React 开发的核心。