React Hooks 实战
大约 16 分钟约 4880 字
React Hooks 实战
简介
Hooks 是 React 16.8 引入的革命性特性,让函数组件拥有状态管理和副作用处理能力。除了 useState/useEffect 等内置 Hooks,自定义 Hooks 可以封装和复用有状态逻辑。掌握 Hooks 是现代 React 开发的核心技能。
Hooks 的设计初衷是解决 class 组件的三大痛点:
- 逻辑复用困难:高阶组件(HOC)和 render props 导致组件嵌套过深("wrapper hell")
- 生命周期割裂逻辑:相关逻辑被拆分到 componentDidMount、componentDidUpdate 等不同方法中
- class 的心智负担:this 绑定、构造函数、生命周期方法等概念增加了学习成本
特点
Hooks 规则
在使用任何 Hook 之前,必须理解两条核心规则:
// 规则 1:只在函数组件或自定义 Hook 的顶层调用 Hook
// ❌ 错误:在条件语句中使用
function BadComponent({ isLoggedIn }: { isLoggedIn: boolean }) {
if (isLoggedIn) {
const [user, setUser] = useState(null) // ❌ 违反规则
}
}
// ✅ 正确:所有 Hook 在顶层调用
function GoodComponent({ isLoggedIn }: { isLoggedIn: boolean }) {
const [user, setUser] = useState<User | null>(null) // ✅ 顶层
// 条件逻辑放在 Hook 内部
useEffect(() => {
if (isLoggedIn) {
fetchUser().then(setUser)
}
}, [isLoggedIn])
}
// 规则 2:只在 React 函数组件或自定义 Hook 中调用
// ❌ 不能在普通函数、class 组件、事件处理函数中使用
function regularFunction() {
useState(0) // ❌ 错误
}
// ✅ 自定义 Hook 以 use 开头,内部可以调用其他 Hook
function useCustomHook() {
const [value, setValue] = useState(0) // ✅
return { value, setValue }
}安装 ESLint 插件可以在开发时自动检测违规:
npm install eslint-plugin-react-hooks --save-dev核心 Hooks
useState 与 useReducer
// useState — 简单状态
const [count, setCount] = useState(0)
const [user, setUser] = useState<User | null>(null)
// 函数式更新(基于前一个状态)
setCount(prev => prev + 1)
// 惰性初始化(昂贵的计算只在首次渲染执行)
const [initialData, setInitialData] = useState(() => {
return heavyComputation() // 只在首次渲染时调用
})
// useState 的批量更新机制
// React 18+ 自动批量更新,不再需要手动 batching
function handleClick() {
setCount(c => c + 1) // 不会立即触发渲染
setCount(c => c + 1) // 同上
setCount(c => c + 1) // 同上
// 最终 count 只增加 1,因为三次更新被批量处理
}
// useState 的常见陷阱
function Counter() {
const [count, setCount] = useState(0)
// ❌ 错误:直接基于当前闭包中的 count
const incrementBad = () => {
setCount(count + 1) // 如果快速点击,可能丢失更新
setCount(count + 1) // 两次都基于同一个 count 值
}
// ✅ 正确:使用函数式更新
const incrementGood = () => {
setCount(prev => prev + 1) // 每次都基于最新的前一个值
setCount(prev => prev + 1)
}
}// useReducer — 复杂状态
interface State {
items: CartItem[]
total: number
}
type Action =
| { type: 'ADD'; payload: CartItem }
| { type: 'REMOVE'; payload: number }
| { type: 'CLEAR' }
function cartReducer(state: State, action: Action): State {
switch (action.type) {
case 'ADD': {
const items = [...state.items, action.payload]
return { items, total: items.reduce((s, i) => s + i.price * i.qty, 0) }
}
case 'REMOVE': {
const items = state.items.filter(i => i.id !== action.payload)
return { items, total: items.reduce((s, i) => s + i.price * i.qty, 0) }
}
case 'CLEAR':
return { items: [], total: 0 }
}
}
const [cart, dispatch] = useReducer(cartReducer, { items: [], total: 0 })
dispatch({ type: 'ADD', payload: { id: 1, name: '商品', price: 99, qty: 1 } })useState vs useReducer 选择策略
| 场景 | 推荐 | 原因 |
|---|---|---|
| 简单值(布尔、数字、字符串) | useState | 简单直接 |
| 独立的状态(互不影响) | 多个 useState | 各自独立更新 |
| 多个状态相互关联 | useReducer | 保证状态一致性 |
| 下一个状态依赖前一个状态 | useReducer | reducer 是纯函数 |
| 复杂的状态更新逻辑 | useReducer | 逻辑集中管理 |
| 需要中间件(如日志) | useReducer | 可扩展 dispatch |
useEffect 与 useCallback
// useEffect — 数据获取
useEffect(() => {
const controller = new AbortController()
async function fetchData() {
const res = await fetch('/api/data', { signal: controller.signal })
setData(await res.json())
}
fetchData()
return () => controller.abort() // 清理
}, [dep]) // 依赖变化时重新执行
// useCallback — 缓存函数引用
const handleClick = useCallback((id: number) => {
setSelected(id)
}, []) // 空依赖 = 函数永不变化
// useMemo — 缓存计算结果
const sortedList = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name))
}, [items]) // items 变化时才重新计算
// useRef — DOM 引用
const inputRef = useRef<HTMLInputElement>(null)
const focusInput = () => inputRef.current?.focus()
// useRef — 持久值(不触发重渲染)
const renderCount = useRef(0)
renderCount.current++useEffect 详解与常见模式
// useEffect 的三种模式
// 1. 每次渲染后执行(不推荐,容易无限循环)
useEffect(() => {
console.log('每次渲染后都执行')
})
// 2. 仅首次挂载后执行(空依赖数组)
useEffect(() => {
console.log('组件挂载')
return () => console.log('组件卸载')
}, [])
// 3. 依赖变化时执行
useEffect(() => {
console.log(`count 变为 ${count}`)
}, [count])
// useEffect 的清理函数
useEffect(() => {
const subscription = eventEmitter.subscribe('event', handler)
// 清理:取消订阅
return () => {
subscription.unsubscribe()
}
}, [])
// 常见模式:订阅与清理
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine)
useEffect(() => {
const handleOnline = () => setIsOnline(true)
const handleOffline = () => setIsOnline(false)
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}, [])
return isOnline
}
// 常见模式:定时器清理
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback)
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
if (delay === null) return
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}, [delay])
}
// 使用
function Timer() {
const [seconds, setSeconds] = useState(0)
useInterval(() => setSeconds(s => s + 1), 1000)
return <div>{seconds}s</div>
}useEffect 闭包陷阱
// ❌ 经典闭包陷阱
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1) // count 始终是 0(首次渲染时的闭包值)
}, 1000)
return () => clearInterval(timer)
}, []) // 空依赖,effect 只执行一次,count 被永久捕获为 0
}
// ✅ 修复方案 1:使用函数式更新
function TimerFixed() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1) // prev 始终是最新的前一个值
}, 1000)
return () => clearInterval(timer)
}, [])
}
// ✅ 修复方案 2:正确声明依赖
function TimerFixed2() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1) // count 是最新的
}, 1000)
return () => clearInterval(timer)
}, [count]) // count 变化时重新创建定时器
}
// ✅ 修复方案 3:useRef 存储最新值
function TimerFixed3() {
const [count, setCount] = useState(0)
const countRef = useRef(count)
countRef.current = count
useEffect(() => {
const timer = setInterval(() => {
setCount(countRef.current + 1)
}, 1000)
return () => clearInterval(timer)
}, [])
}useMemo 与 useCallback
// useMemo —— 缓存计算结果(避免每次渲染都重新计算)
function ExpensiveList({ items, filter }: { items: Item[]; filter: string }) {
// ❌ 不用 useMemo:每次渲染都重新过滤
// const filtered = items.filter(item => item.name.includes(filter))
// ✅ 使用 useMemo:只有 items 或 filter 变化时才重新计算
const filtered = useMemo(() => {
console.log('重新计算过滤结果')
return items.filter(item => item.name.includes(filter))
}, [items, filter])
return (
<ul>
{filtered.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
)
}
// useCallback —— 缓存函数引用
// 什么时候需要 useCallback:
// 1. 将函数作为 props 传给做了 React.memo 优化的子组件
// 2. 函数作为 useEffect 的依赖
// ❌ 不需要 useCallback 的场景(子组件没有 memo)
function Parent() {
const [count, setCount] = useState(0)
const handleClick = () => console.log('clicked') // 不需要 useCallback
return <Child onClick={handleClick} />
}
// ✅ 需要 useCallback 的场景(子组件有 memo)
const Child = React.memo(function Child({ onClick }: { onClick: () => void }) {
console.log('Child 渲染')
return <button onClick={onClick}>Click</button>
})
function ParentMemo() {
const [count, setCount] = useState(0)
// 如果不用 useCallback,每次 Parent 重新渲染都会创建新的函数引用
// 导致 memo 失效,Child 不必要地重新渲染
const handleClick = useCallback(() => {
console.log('clicked')
}, []) // 空依赖,函数引用稳定
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<Child onClick={handleClick} />
</div>
)
}
// useMemo 的对象/数组陷阱
function BadExample() {
// ❌ 每次渲染都创建新对象,子组件永远会重新渲染
const style = { color: 'red', fontSize: '16px' }
const options = ['a', 'b', 'c']
// ✅ 使用 useMemo 缓存
const styleMemo = useMemo(() => ({ color: 'red', fontSize: '16px' }), [])
const optionsMemo = useMemo(() => ['a', 'b', 'c'], [])
}useRef 详解
// useRef 的两种用途
// 1. 获取 DOM 引用
function TextInputWithFocusButton() {
const inputEl = useRef<HTMLInputElement>(null)
const onButtonClick = () => {
inputEl.current?.focus()
}
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
)
}
// 2. 存储可变值(不触发重渲染)
function Stopwatch() {
const [time, setTime] = useState(0)
const timerRef = useRef<number | null>(null) // 存储定时器 ID
const startTimeRef = useRef<number>(0) // 存储开始时间
const start = () => {
startTimeRef.current = Date.now()
timerRef.current = setInterval(() => {
setTime(Date.now() - startTimeRef.current)
}, 10)
}
const stop = () => {
if (timerRef.current) {
clearInterval(timerRef.current)
timerRef.current = null
}
}
// 注意:修改 ref.current 不会触发重渲染
// 这是 ref 和 state 的本质区别
return <div>{time}ms</div>
}
// 3. ref 回调
function MeasureDiv() {
const [height, setHeight] = useState(0)
const measuredRef = useCallback((node: HTMLDivElement | null) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height)
}
}, [])
return (
<>
<div ref={measuredRef}>Measured content</div>
<p>Height: {height}px</p>
</>
)
}
// 4. 存储前一个值
function usePrevious<T>(value: T): T {
const ref = useRef<T>(value)
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
const prevScrollY = usePrevious(scrollY)
useEffect(() => {
const handler = () => setScrollY(window.scrollY)
window.addEventListener('scroll', handler)
return () => window.removeEventListener('scroll', handler)
}, [])
return (
<div>
<p>当前: {scrollY}</p>
<p>之前: {prevScrollY}</p>
<p>方向: {scrollY > prevScrollY ? '向下' : '向上'}</p>
</div>
)
}上下文 Hooks
useContext
// 基础用法
const ThemeContext = createContext<'light' | 'dark'>('light')
const LanguageContext = createContext<'zh' | 'en'>('zh')
function ThemeButton() {
const theme = useContext(ThemeContext)
return <button className={theme === 'dark' ? 'dark-btn' : 'light-btn'}>
{theme === 'dark' ? '🌙' : '☀️'}
</button>
}
function App() {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
return (
<ThemeContext.Provider value={theme}>
<ThemeButton />
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
切换主题
</button>
</ThemeContext.Provider>
)
}
// 生产级 Context 模式:分离 Provider 和 Hook
// contexts/ThemeContext.tsx
interface ThemeContextType {
theme: 'light' | 'dark'
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType | null>(null)
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
const saved = localStorage.getItem('theme')
return (saved === 'dark' || !saved && window.matchMedia('(prefers-color-scheme: dark)').matches)
? 'dark' : 'light'
})
const toggleTheme = useCallback(() => {
setTheme(prev => {
const next = prev === 'light' ? 'dark' : 'light'
localStorage.setItem('theme', next)
document.documentElement.classList.toggle('dark', next === 'dark')
return next
})
}, [])
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return context
}
// 组件中使用
function Header() {
const { theme, toggleTheme } = useTheme()
return <button onClick={toggleTheme}>{theme === 'dark' ? '☀️' : '🌙'}</button>
}
// Context 的性能问题与优化
// ❌ 当 context value 变化时,所有消费该 context 的组件都会重新渲染
// 即使只有一部分数据变了
// ✅ 拆分 Context:将频繁变化和稳定的数据分开
const AuthContext = createContext<{ user: User | null }>({ user: null })
const CartContext = createContext<{ items: CartItem[] }>({ items: [] })
// ✅ 使用 useMemo 避免不必要的 value 变化
function Provider({ children }) {
const [user, setUser] = useState(null)
const [items, setItems] = useState([])
const authValue = useMemo(() => ({ user }), [user])
const cartValue = useMemo(() => ({ items }), [items])
return (
<AuthContext.Provider value={authValue}>
<CartContext.Provider value={cartValue}>
{children}
</CartContext.Provider>
</AuthContext.Provider>
)
}自定义 Hooks
useAsync
// hooks/useAsync.ts — 异步数据加载
function useAsync<T>(asyncFn: () => Promise<T>, deps: any[] = []) {
const [data, setData] = useState<T | null>(null)
const [error, setError] = useState<Error | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
let cancelled = false
setLoading(true)
setError(null)
asyncFn()
.then(result => { if (!cancelled) setData(result) })
.catch(err => { if (!cancelled) setError(err) })
.finally(() => { if (!cancelled) setLoading(false) })
return () => { cancelled = true }
}, deps)
return { data, error, loading }
}
// 使用
const { data: users, loading, error } = useAsync(
() => fetch('/api/users').then(r => r.json()),
[page]
)useDebounce
// hooks/useDebounce.ts — 防抖
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
// 使用 — 搜索防抖
const [search, setSearch] = useState('')
const debouncedSearch = useDebounce(search, 300)
useEffect(() => {
if (debouncedSearch) fetchResults(debouncedSearch)
}, [debouncedSearch])useLocalStorage
// hooks/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
})
const setStoredValue = (newValue: T | ((prev: T) => T)) => {
const valueToStore = newValue instanceof Function ? newValue(value) : newValue
setValue(valueToStore)
localStorage.setItem(key, JSON.stringify(valueToStore))
}
return [value, setStoredValue] as const
}
// 使用
const [theme, setTheme] = useLocalStorage('theme', 'light')
const [recentSearches, setRecentSearches] = useLocalStorage<string[]>('recent', [])useClickOutside
// hooks/useClickOutside.ts — 点击外部检测
function useClickOutside(ref: RefObject<HTMLElement>, callback: () => void) {
useEffect(() => {
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
callback()
}
}
document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [ref, callback])
}
// 使用
const dropdownRef = useRef<HTMLDivElement>(null)
const [open, setOpen] = useState(false)
useClickOutside(dropdownRef, () => setOpen(false))更多实用自定义 Hooks
// useToggle —— 布尔值切换
function useToggle(initialValue = false): [boolean, () => void] {
const [value, setValue] = useState(initialValue)
const toggle = useCallback(() => setValue(v => !v), [])
return [value, toggle]
}
// 使用
const [isOpen, toggleOpen] = useToggle(false)
const [isVisible, toggleVisible] = useToggle(true)
// useFetch —— 带缓存的数据请求
const fetchCache = new Map<string, { data: any; timestamp: number }>()
function useFetch<T>(url: string, options?: RequestInit) {
const [data, setData] = useState<T | null>(null)
const [error, setError] = useState<Error | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const cacheKey = url + JSON.stringify(options)
const cached = fetchCache.get(cacheKey)
// 5 分钟缓存
if (cached && Date.now() - cached.timestamp < 300000) {
setData(cached.data)
setLoading(false)
return
}
const controller = new AbortController()
fetch(url, { ...options, signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
})
.then(json => {
setData(json)
fetchCache.set(cacheKey, { data: json, timestamp: Date.now() })
})
.catch(err => {
if (err.name !== 'AbortError') setError(err)
})
.finally(() => setLoading(false))
return () => controller.abort()
}, [url])
return { data, error, loading }
}
// useForm —— 表单管理
function useForm<T extends Record<string, any>>(initialValues: T) {
const [values, setValues] = useState(initialValues)
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({})
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({})
const handleChange = useCallback((name: keyof T, value: any) => {
setValues(prev => ({ ...prev, [name]: value }))
setErrors(prev => ({ ...prev, [name]: '' })) // 清除该字段的错误
}, [])
const handleBlur = useCallback((name: keyof T) => {
setTouched(prev => ({ ...prev, [name]: true }))
}, [])
const validate = useCallback((rules: Record<keyof T, (val: any) => string | undefined>) => {
const newErrors: Partial<Record<keyof T, string>> = {}
let isValid = true
for (const [field, validator] of Object.entries(rules)) {
const error = validator(values[field as keyof T])
if (error) {
newErrors[field as keyof T] = error
isValid = false
}
}
setErrors(newErrors)
return isValid
}, [values])
const reset = useCallback(() => {
setValues(initialValues)
setErrors({})
setTouched({})
}, [initialValues])
return { values, errors, touched, handleChange, handleBlur, validate, reset }
}
// 使用
function LoginForm() {
const { values, errors, touched, handleChange, handleBlur, validate } = useForm({
email: '',
password: '',
})
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
const isValid = validate({
email: (v) => !v ? '请输入邮箱' : !v.includes('@') ? '邮箱格式错误' : undefined,
password: (v) => !v ? '请输入密码' : v.length < 8 ? '密码至少 8 位' : undefined,
})
if (isValid) {
console.log('提交', values)
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={values.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={() => handleBlur('email')}
/>
{touched.email && errors.email && <span>{errors.email}</span>}
<input
type="password"
value={values.password}
onChange={(e) => handleChange('password', e.target.value)}
onBlur={() => handleBlur('password')}
/>
{touched.password && errors.password && <span>{errors.password}</span>}
<button type="submit">登录</button>
</form>
)
}
// useMediaQuery —— 响应式媒体查询
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(() => window.matchMedia(query).matches)
useEffect(() => {
const mql = window.matchMedia(query)
const handler = (e: MediaQueryListEvent) => setMatches(e.matches)
mql.addEventListener('change', handler)
return () => mql.removeEventListener('change', handler)
}, [query])
return matches
}
// 使用
function ResponsiveLayout() {
const isMobile = useMediaQuery('(max-width: 768px)')
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)')
return (
<div className={isMobile ? 'mobile-layout' : 'desktop-layout'}>
{isMobile ? <MobileNav /> : <DesktopNav />}
</div>
)
}
// useIntersectionObserver —— 元素可见性检测
function useIntersectionObserver(
options?: IntersectionObserverInit
): [RefObject<HTMLDivElement | null>, boolean] {
const ref = useRef<HTMLDivElement | null>(null)
const [isVisible, setIsVisible] = useState(false)
useEffect(() => {
const element = ref.current
if (!element) return
const observer = new IntersectionObserver(([entry]) => {
setIsVisible(entry.isIntersecting)
}, options)
observer.observe(element)
return () => observer.disconnect()
}, [options])
return [ref, isVisible]
}
// 使用 —— 懒加载组件
function LazySection() {
const [ref, isVisible] = useIntersectionObserver({ threshold: 0.1 })
return (
<div ref={ref}>
{isVisible ? <ExpensiveComponent /> : <div>Loading...</div>}
</div>
)
}React 18 新 Hooks
useTransition
// useTransition —— 标记非紧急的状态更新
function SearchPage() {
const [query, setQuery] = useState('')
const [results, setResults] = useState<Result[]>([])
const [isPending, startTransition] = useTransition()
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// 输入框的更新是紧急的(用户需要立即看到输入内容)
setQuery(e.target.value)
// 搜索结果的更新是非紧急的(可以延迟)
startTransition(() => {
const filtered = heavySearch(e.target.value)
setResults(filtered)
})
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <div>搜索中...</div>}
<ul>
{results.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
)
}useDeferredValue
// useDeferredValue —— 延迟更新某个值
function FilteredList({ items }: { items: string[] }) {
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
const filtered = useMemo(() => {
return items.filter(item =>
item.toLowerCase().includes(deferredQuery.toLowerCase())
)
}, [items, deferredQuery])
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索..."
/>
<ul>
{filtered.map(item => <li key={item}>{item}</li>)}
</ul>
</div>
)
}
// useTransition vs useDeferredValue 对比
// useTransition:控制状态更新的优先级(你控制哪个更新是非紧急的)
// useDeferredValue:接受一个值,返回一个延迟更新的版本(让接收方处理延迟)useId
// useId —— 生成唯一 ID(用于表单 label 和 input 的关联)
function FormField({ label }: { label: string }) {
const id = useId()
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
)
}
// SSR 安全:在服务端和客户端生成相同的 ID
// 避免了水合不匹配的问题useSyncExternalStore
// useSyncExternalStore —— 订阅外部数据源
// 适用于非 React 状态管理(如 Redux、Zustand、RxJS)
// 订阅浏览器在线状态
function useOnlineStatus() {
return useSyncExternalStore(
subscribe, // 订阅函数
() => navigator.onLine, // 获取当前值的函数(客户端)
() => true // 获取服务端快照(SSR)
)
}
function subscribe(callback: () => void) {
window.addEventListener('online', callback)
window.addEventListener('offline', callback)
return () => {
window.removeEventListener('online', callback)
window.removeEventListener('offline', callback)
}
}
// 使用
function StatusBar() {
const isOnline = useOnlineStatus()
return <div>{isOnline ? '在线' : '离线'}</div>
}性能优化策略
React.memo 与 Hooks 配合
// React.memo —— 避免不必要的子组件重渲染
const ExpensiveChild = React.memo(function ExpensiveChild({
title,
onUpdate,
}: {
title: string
onUpdate: (id: string) => void
}) {
console.log('ExpensiveChild 渲染')
return <div onClick={() => onUpdate('1')}>{title}</div>
})
function Parent() {
const [count, setCount] = useState(0)
const [title] = useState('标题')
// 如果不加 useCallback,每次 Parent 渲染都会创建新的函数引用
// 导致 ExpensiveChild 的 React.memo 失效
const handleUpdate = useCallback((id: string) => {
console.log('update', id)
}, [])
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<ExpensiveChild title={title} onUpdate={handleUpdate} />
</div>
)
}组件拆分与状态提升
// ❌ 所有状态都在一个组件中,任何状态变化都导致整个组件重渲染
function BadForm() {
const [formData, setFormData] = useState({ name: '', email: '', phone: '' })
const [isSubmitting, setIsSubmitting] = useState(false)
const [errors, setErrors] = useState({})
// 修改任何字段都会导致整个表单重新渲染
return (
<form>
<input value={formData.name} onChange={e => setFormData({...formData, name: e.target.value})} />
<input value={formData.email} onChange={e => setFormData({...formData, email: e.target.value})} />
<input value={formData.phone} onChange={e => setFormData({...formData, phone: e.target.value})} />
</form>
)
}
// ✅ 拆分为独立字段组件
function FormField({ value, onChange, error }: FieldProps) {
return (
<div>
<input value={value} onChange={onChange} />
{error && <span className="error">{error}</span>}
</div>
)
}
// 用 React.memo 包裹,只有当 value 或 error 变化时才重渲染
const MemoizedFormField = React.memo(FormField)
function GoodForm() {
const [formData, setFormData] = useState({ name: '', email: '', phone: '' })
return (
<form>
<MemoizedFormField
value={formData.name}
onChange={e => setFormData(d => ({ ...d, name: e.target.value }))}
error={null}
/>
<MemoizedFormField
value={formData.email}
onChange={e => setFormData(d => ({ ...d, email: e.target.value }))}
error={null}
/>
</form>
)
}优点
缺点
总结
React Hooks 是函数组件的灵魂。核心 Hooks:useState(状态)、useEffect(副作用)、useRef(引用)、useMemo/useCallback(性能优化)。自定义 Hooks 提取可复用逻辑(useAsync、useDebounce、useLocalStorage)。关键规则:Hooks 只在顶层调用,依赖数组必须完整。
最佳实践清单:
- 安装 eslint-plugin-react-hooks,让 ESLint 帮你检查 Hook 规则
- useEffect 依赖数组使用 eslint 的 exhaustive-deps 规则
- 复杂状态逻辑用 useReducer 替代多个 useState
- 函数式更新(setState(prev => ...))避免闭包陷阱
- useMemo/useCallback 只在确实有性能问题时使用,不要过度优化
- 自定义 Hook 以 use 开头,遵循单一职责原则
关键知识点
- 先判断主题更偏浏览器原理、框架机制、工程化还是性能优化。
- 前端问题很多看似是页面问题,实际源头在构建、缓存、状态流或接口协作。
- 真正成熟的前端方案一定同时考虑首屏、交互、可维护性和线上诊断。
- 前端主题最好同时看浏览器原理、框架机制和工程化约束。
项目落地视角
- 把组件边界、状态归属、网络层规范和错误处理先定下来。
- 上线前检查包体积、缓存命中、接口失败路径和关键交互降级策略。
- 如果主题和性能有关,最好用 DevTools、Lighthouse 或埋点验证。
- 对关键页面先建立状态流和数据流,再考虑组件拆分。
常见误区
- 只盯框架 API,不理解浏览器和运行时成本。
- 把状态、请求和 UI 更新混成一层,后期难维护。
- 线上问题出现时没有日志、埋点和性能基线可对照。
- 只追框架新特性,不分析实际渲染成本。
进阶路线
- 继续补齐 SSR、边缘渲染、设计系统和监控告警能力。
- 把主题和后端接口约定、CI/CD、缓存策略一起思考。
- 沉淀组件规范、页面模板和性能基线,减少团队差异。
- 继续补齐设计系统、SSR/边缘渲染、监控告警和组件库治理。
适用场景
- 当你准备把《React Hooks 实战》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合中后台应用、门户站点、组件库和实时交互页面。
- 当需求涉及状态流、路由、网络缓存、SSR/CSR 或性能治理时,这类主题很关键。
落地建议
- 先定义组件边界和状态归属,再落地 UI 细节。
- 对核心页面做首屏、体积、缓存和错误路径检查。
- 把安全、兼容性和可访问性纳入默认交付标准。
排错清单
- 先用浏览器 DevTools 看请求、性能面板和控制台错误。
- 检查依赖版本、构建配置、环境变量和静态资源路径。
- 如果是线上问题,优先确认缓存、CDN 和构建产物是否一致。
复盘问题
- 如果把《React Hooks 实战》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《React Hooks 实战》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《React Hooks 实战》最大的收益和代价分别是什么?
