深入理解 React Hooks 完整体系
所有 19 个内置 Hook 完整 API、底层原理、自定义 Hook 设计模式与实战集合
什么是 React Hooks?
定义:Hooks 是 React 16.8 引入的函数式 API,让函数组件拥有状态管理、副作用处理、上下文访问等能力。React 19 新增了 useActionState、useOptimistic、use 三个 Hook,总计 19 个内置 Hook。
涉及场景:
- 状态管理:组件内部状态(useState)、复杂状态逻辑(useReducer)
- 副作用处理:数据获取、DOM 操作、订阅(useEffect / useLayoutEffect)
- 性能优化:缓存计算结果和函数引用(useMemo / useCallback)
- 并发交互:非紧急更新(useTransition)、延迟值(useDeferredValue)
- 表单处理:Action 状态管理(useActionState)、乐观更新(useOptimistic)
- 上下文共享:跨组件通信(useContext / use)
作用:
- 取代类组件:函数组件 + Hooks 成为 React 开发的唯一范式
- 逻辑复用:自定义 Hook 替代 HOC 和 render props,更简洁、更可组合
- 关注点分离:按功能组织代码,而非按生命周期分散逻辑
- 面试核心:Hooks 原理(链表、闭包)和正确使用是中高级面试必考
一、状态类 Hooks
useState
// 基本用法
const [count, setCount] = useState(0);
// 惰性初始化(只在首次渲染执行)
const [data, setData] = useState(() => {
return JSON.parse(localStorage.getItem('data')) || [];
});
// 函数式更新(基于前一个状态)
setCount(prev => prev + 1);
// 对象状态(必须返回新引用)
const [form, setForm] = useState({ name: '', email: '' });
setForm(prev => ({ ...prev, name: '张三' }));底层原理:
首次渲染:
创建 Hook 节点 → { memoizedState: initialState, queue: updateQueue, next: null }
挂载到 Fiber 节点的 memoizedState 链表上
后续渲染:
按顺序从链表取出对应 Hook 节点
处理 queue 中的所有 update(批量合并)
用 Object.is 对比新旧值,相同则 bailout(跳过渲染)注意事项:
- 更新是异步批量的(React 18+ 所有场景自动批量)
Object.is(oldState, newState)为 true 时跳过更新- 不要直接修改对象/数组,必须创建新引用
useReducer
type State = { count: number; step: number };
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'setStep'; payload: number }
| { type: 'reset' };
function reducer(state: State, action: Action): State {
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 };
case 'reset': return { count: 0, step: 1 };
default: return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
// 惰性初始化
// const [state, dispatch] = useReducer(reducer, initialArg, init);
return (
<div>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: 'increment' })}>+{state.step}</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-{state.step}</button>
<input
type="number"
value={state.step}
onChange={e => dispatch({ type: 'setStep', payload: Number(e.target.value) })}
/>
</div>
);
}vs useState:
dispatch的引用永远稳定,传给子组件不需要 useCallback- 多个状态相互关联时,reducer 让状态转换更可预测
- 适合配合 Context 实现简易全局状态管理
useSyncExternalStore
订阅外部 store(非 React 状态源),保证并发模式下的数据一致性。
import { useSyncExternalStore } from 'react';
// 订阅浏览器在线状态
function useOnlineStatus() {
return useSyncExternalStore(
// subscribe:订阅函数,返回取消订阅的函数
(callback) => {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
},
// getSnapshot:获取当前状态(客户端)
() => navigator.onLine,
// getServerSnapshot:SSR 时的状态(可选)
() => true
);
}
// 订阅自定义 store
const store = {
state: { count: 0 },
listeners: new Set(),
subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
},
getSnapshot() {
return store.state;
},
increment() {
store.state = { ...store.state, count: store.state.count + 1 };
store.listeners.forEach(l => l());
},
};
function Counter() {
const { count } = useSyncExternalStore(store.subscribe, store.getSnapshot);
return <button onClick={() => store.increment()}>{count}</button>;
}使用场景:第三方状态管理库(Zustand/Redux 底层都用了它)、浏览器 API 订阅、WebSocket 消息。
二、副作用类 Hooks
useEffect
// 1. 基本:每次渲染后执行
useEffect(() => {
document.title = `计数: ${count}`;
});
// 2. 依赖数组:仅在 userId 变化时执行
useEffect(() => {
let cancelled = false;
fetchUser(userId).then(user => {
if (!cancelled) setUser(user);
});
return () => { cancelled = true; }; // 清理
}, [userId]);
// 3. 空数组:仅挂载时执行一次
useEffect(() => {
const ws = new WebSocket('wss://api.example.com');
ws.onmessage = (e) => handleMessage(JSON.parse(e.data));
return () => ws.close(); // 卸载时关闭
}, []);执行时机:
渲染 → DOM 更新 → 浏览器绘制 → useEffect 执行(异步)
↑
不阻塞浏览器渲染清理函数的调用时机:
- 下次 effect 执行之前
- 组件卸载时
useLayoutEffect
与 useEffect API 完全相同,但在 DOM 更新后、浏览器绘制前同步执行。
// 用途:需要在绘制前读取/修改 DOM
function Tooltip({ anchorRef, children }) {
const tooltipRef = useRef(null);
const [position, setPosition] = useState({ top: 0, left: 0 });
useLayoutEffect(() => {
// 在浏览器绘制前计算位置,避免闪烁
const anchor = anchorRef.current.getBoundingClientRect();
const tooltip = tooltipRef.current.getBoundingClientRect();
setPosition({
top: anchor.top - tooltip.height - 8,
left: anchor.left + (anchor.width - tooltip.width) / 2,
});
}, [anchorRef]);
return (
<div ref={tooltipRef} style={{ position: 'fixed', ...position }}>
{children}
</div>
);
}执行时机:
渲染 → DOM 更新 → useLayoutEffect 执行(同步)→ 浏览器绘制
↑
阻塞绘制!避免滥用选择规则:默认用 useEffect;只在需要同步读取 DOM 布局或防止视觉闪烁时才用 useLayoutEffect。
useInsertionEffect
React 18 新增,在 DOM 变更之前执行。专为 CSS-in-JS 库设计(如 styled-components、Emotion)。
// 仅供 CSS-in-JS 库作者使用,应用开发者几乎不需要
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = `.my-class { color: red; }`;
document.head.appendChild(style);
return () => document.head.removeChild(style);
}, []);执行顺序:useInsertionEffect → DOM 变更 → useLayoutEffect → 绘制 → useEffect
三、引用类 Hooks
useRef
// 1. DOM 引用
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>聚焦</button>
</>
);
}
// 2. 可变值容器(不触发渲染)
function Timer() {
const intervalRef = useRef<number | null>(null);
const countRef = useRef(0);
const start = () => {
intervalRef.current = window.setInterval(() => {
countRef.current += 1;
console.log(countRef.current);
}, 1000);
};
const stop = () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
useEffect(() => stop, []); // 卸载时清理
return <button onClick={start}>开始</button>;
}
// 3. 保存前一次渲染的值
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => { ref.current = value; });
return ref.current;
}useImperativeHandle
自定义通过 ref 暴露给父组件的实例方法。
// React 19:ref 直接作为 prop
function VideoPlayer({ ref }) {
const videoRef = useRef<HTMLVideoElement>(null);
useImperativeHandle(ref, () => ({
play() { videoRef.current?.play(); },
pause() { videoRef.current?.pause(); },
seekTo(time: number) {
if (videoRef.current) videoRef.current.currentTime = time;
},
}), []);
return <video ref={videoRef} />;
}
// 父组件
function App() {
const playerRef = useRef(null);
return (
<>
<VideoPlayer ref={playerRef} />
<button onClick={() => playerRef.current?.play()}>播放</button>
<button onClick={() => playerRef.current?.seekTo(30)}>跳到30秒</button>
</>
);
}四、上下文类 Hooks
useContext
const ThemeContext = createContext<'light' | 'dark'>('light');
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={`btn-${theme}`}>按钮</button>;
}
// Provider
<ThemeContext.Provider value="dark">
<ThemedButton />
</ThemeContext.Provider>use(React 19)
use 可在条件语句中调用,读取 Context 或 Promise。
import { use } from 'react';
// 读取 Context(可在条件中)
function Panel({ showTheme }) {
if (showTheme) {
const theme = use(ThemeContext);
return <div className={theme}>主题面板</div>;
}
return <div>默认面板</div>;
}
// 读取 Promise(配合 Suspense)
function UserName({ userPromise }) {
const user = use(userPromise);
return <span>{user.name}</span>;
}
<Suspense fallback={<Spinner />}>
<UserName userPromise={fetchUser(id)} />
</Suspense>五、性能优化类 Hooks
useMemo
// 缓存计算结果
const sortedList = useMemo(() => {
return items
.filter(item => item.category === category)
.sort((a, b) => a.name.localeCompare(b.name));
}, [items, category]);
// 缓存 JSX(避免子树重渲染)
const chart = useMemo(() => <ExpensiveChart data={data} />, [data]);useCallback
// 缓存函数引用
const handleSubmit = useCallback(async (formData: FormData) => {
await submitForm(formData);
refreshList();
}, [refreshList]);
// 本质等价于
const handleSubmit = useMemo(() => async (formData: FormData) => {
await submitForm(formData);
refreshList();
}, [refreshList]);React Compiler 时代:这两个 Hook 将被编译器自动插入,开发者不再需要手动编写。
六、并发类 Hooks
useTransition
function TabContainer() {
const [tab, setTab] = useState('home');
const [isPending, startTransition] = useTransition();
function selectTab(nextTab: string) {
startTransition(() => {
setTab(nextTab); // 非紧急更新,可被中断
});
}
return (
<div>
<nav>
{['home', 'about', 'posts'].map(t => (
<button key={t} onClick={() => selectTab(t)}
style={{ fontWeight: tab === t ? 'bold' : 'normal' }}>
{t}
</button>
))}
</nav>
{isPending && <Spinner />}
<TabContent tab={tab} />
</div>
);
}useDeferredValue
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div style={{ opacity: isStale ? 0.6 : 1, transition: 'opacity 0.2s' }}>
<SlowList query={deferredQuery} />
</div>
);
}useId
生成在 SSR 和 CSR 之间稳定的唯一 ID,避免水合不匹配。
function FormField({ label }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} />
</div>
);
}
// 一个组件内多个 ID
function PasswordField() {
const id = useId();
return (
<>
<label htmlFor={`${id}-password`}>密码</label>
<input id={`${id}-password`} type="password" />
<label htmlFor={`${id}-confirm`}>确认密码</label>
<input id={`${id}-confirm`} type="password" />
</>
);
}七、React 19 新增 Hooks
useActionState
import { useActionState } from 'react';
function AddToCartForm({ productId }) {
const [state, formAction, isPending] = useActionState(
async (prevState, formData) => {
const quantity = Number(formData.get('quantity'));
try {
await addToCart(productId, quantity);
return { success: true, message: `已添加 ${quantity} 件`, error: null };
} catch (err) {
return { ...prevState, error: err.message };
}
},
{ success: false, message: '', error: null }
);
return (
<form action={formAction}>
<input name="quantity" type="number" defaultValue={1} min={1} />
<button disabled={isPending}>{isPending ? '添加中...' : '加入购物车'}</button>
{state.success && <p className="success">{state.message}</p>}
{state.error && <p className="error">{state.error}</p>}
</form>
);
}useOptimistic
import { useOptimistic } from 'react';
function MessageList({ messages, sendMessage }) {
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(current, newMsg) => [...current, { ...newMsg, sending: true }]
);
async function handleSend(formData) {
const text = formData.get('text');
const msg = { id: crypto.randomUUID(), text, author: 'me' };
addOptimistic(msg);
await sendMessage(msg);
}
return (
<div>
{optimisticMessages.map(msg => (
<div key={msg.id} style={{ opacity: msg.sending ? 0.5 : 1 }}>
<strong>{msg.author}:</strong> {msg.text}
</div>
))}
<form action={handleSend}>
<input name="text" />
<button>发送</button>
</form>
</div>
);
}八、调试类 Hook
useDebugValue
在 React DevTools 中为自定义 Hook 显示标签。
function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
useDebugValue(isOnline ? '🟢 在线' : '🔴 离线');
return isOnline;
}
// 带格式化(延迟计算,只在 DevTools 打开时执行)
function useFormattedDate(date: Date) {
useDebugValue(date, d => d.toLocaleDateString('zh-CN'));
// ...
}九、Hooks 底层原理
链表结构
Fiber 节点
└── memoizedState → Hook1 → Hook2 → Hook3 → null
↓ ↓ ↓
useState useEffect useRef
每个 Hook 节点:
{
memoizedState: any, // 当前状态值
baseState: any, // 基础状态(用于并发更新合并)
baseQueue: Update[], // 待处理的更新
queue: UpdateQueue, // 更新队列
next: Hook | null // 指向下一个 Hook
}为什么不能在条件中调用 Hook
// ❌ 条件调用导致链表错位
function Bad({ show }) {
if (show) {
useState('A'); // 第1个 Hook(有时存在,有时不存在)
}
useState('B'); // 第2个 Hook(与链表位置不匹配)
}
// 渲染1(show=true): Hook链表 = [A] → [B]
// 渲染2(show=false): Hook链表 = [B](但 React 认为第1个是 A)
// → 状态错乱!闭包与 Stale Closure
function Counter() {
const [count, setCount] = useState(0);
// 每次渲染创建新的闭包,捕获当时的 count 值
const handleClick = () => {
// 这里的 count 是「这次渲染时」的值
setTimeout(() => {
console.log(count); // 闭包中的旧值
}, 3000);
};
// 函数式更新不受闭包影响
const increment = () => setCount(c => c + 1); // c 始终是最新值
}Mount vs Update 的双重实现
React 内部维护两套 Hook 实现:
Mount 阶段(首次渲染):
HooksDispatcherOnMount = {
useState: mountState, // 创建 Hook 节点,初始化状态
useEffect: mountEffect, // 创建 Hook 节点,标记 effect
useRef: mountRef, // 创建 { current: initialValue }
}
Update 阶段(后续渲染):
HooksDispatcherOnUpdate = {
useState: updateState, // 从链表取出 Hook,处理更新队列
useEffect: updateEffect, // 对比依赖数组,决定是否重新执行
useRef: updateRef, // 直接返回已有的 ref 对象
}十、自定义 Hook 实战集合
useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback((value: T | ((prev: T) => T)) => {
setStoredValue(prev => {
const newValue = value instanceof Function ? value(prev) : value;
localStorage.setItem(key, JSON.stringify(newValue));
return newValue;
});
}, [key]);
return [storedValue, setValue] as const;
}
// 使用
const [theme, setTheme] = useLocalStorage('theme', 'light');useDebounce
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;
}
// 使用
function Search() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) fetchResults(debouncedQuery);
}, [debouncedQuery]);
}useMediaQuery
function useMediaQuery(query: string): boolean {
return useSyncExternalStore(
(callback) => {
const mql = window.matchMedia(query);
mql.addEventListener('change', callback);
return () => mql.removeEventListener('change', callback);
},
() => window.matchMedia(query).matches,
() => false // SSR 默认值
);
}
// 使用
const isMobile = useMediaQuery('(max-width: 768px)');
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');useIntersectionObserver
function useIntersectionObserver(
options?: IntersectionObserverInit
): [React.RefCallback<Element>, IntersectionObserverEntry | null] {
const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
const observerRef = useRef<IntersectionObserver | null>(null);
const ref = useCallback((node: Element | null) => {
if (observerRef.current) observerRef.current.disconnect();
if (!node) return;
observerRef.current = new IntersectionObserver(([entry]) => {
setEntry(entry);
}, options);
observerRef.current.observe(node);
}, [options?.threshold, options?.root, options?.rootMargin]);
return [ref, entry];
}
// 使用:懒加载图片
function LazyImage({ src, alt }) {
const [ref, entry] = useIntersectionObserver({ threshold: 0.1 });
const isVisible = entry?.isIntersecting;
return (
<div ref={ref}>
{isVisible ? <img src={src} alt={alt} /> : <Skeleton />}
</div>
);
}useEventListener
function useEventListener<K extends keyof WindowEventMap>(
eventName: K,
handler: (event: WindowEventMap[K]) => void,
element: EventTarget = window
) {
const savedHandler = useRef(handler);
useLayoutEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const listener = (event: Event) => savedHandler.current(event as WindowEventMap[K]);
element.addEventListener(eventName, listener);
return () => element.removeEventListener(eventName, listener);
}, [eventName, element]);
}
// 使用
useEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal();
});十一、Hooks 完整速查表
| Hook | 用途 | 触发渲染 | React 版本 |
|---|---|---|---|
useState | 状态管理 | ✅ | 16.8 |
useReducer | 复杂状态 | ✅ | 16.8 |
useEffect | 副作用(异步) | ❌ | 16.8 |
useLayoutEffect | 副作用(同步,绘制前) | ❌ | 16.8 |
useInsertionEffect | CSS-in-JS 注入 | ❌ | 18 |
useRef | DOM 引用 / 可变值 | ❌ | 16.8 |
useImperativeHandle | 自定义 ref 暴露 | ❌ | 16.8 |
useContext | 读取上下文 | ✅ | 16.8 |
use | 读取 Promise/Context | ✅ | 19 |
useMemo | 缓存计算结果 | ❌ | 16.8 |
useCallback | 缓存函数引用 | ❌ | 16.8 |
useTransition | 非紧急更新 | ✅ | 18 |
useDeferredValue | 延迟值 | ✅ | 18 |
useId | 稳定唯一 ID | ❌ | 18 |
useSyncExternalStore | 订阅外部 store | ✅ | 18 |
useActionState | Action 状态 | ✅ | 19 |
useOptimistic | 乐观更新 | ✅ | 19 |
useDebugValue | DevTools 标签 | ❌ | 16.8 |
面试高频题
Q: useEffect 和 useLayoutEffect 的区别?
useEffect:异步执行,在浏览器绘制后,不阻塞渲染useLayoutEffect:同步执行,在 DOM 更新后、浏览器绘制前,会阻塞渲染- 默认用
useEffect,需要同步测量/修改 DOM 时才用useLayoutEffect
Q: 为什么 Hook 不能在条件语句中调用?
React 通过调用顺序(链表索引)匹配 Hook 与状态。条件调用会导致顺序不一致,后续所有 Hook 与错误的状态配对。use() 是唯一的例外(React 19)。
Q: useMemo 和 useCallback 的本质区别?
useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。useMemo 缓存计算结果,useCallback 缓存函数本身。React Compiler 会自动处理这些优化。
Q: 如何避免闭包陷阱?
- 函数式更新:
setState(prev => prev + 1) - useRef:
ref.current始终指向最新值 - 正确设置依赖数组:把所有引用的外部变量加入 deps