Hooks 原理
问题
React Hooks 是如何实现的?useState 和 useEffect 的原理是什么?什么是闭包陷阱?
答案
Hooks 是 React 16.8 引入的特性,让函数组件也能拥有状态和生命周期。Hooks 的核心实现依赖于 Fiber 架构和链表结构。
Hooks 的基本规则
在理解原理之前,先了解 Hooks 的使用规则:
- 只能在函数组件或自定义 Hook 中使用
- 只能在顶层调用,不能在条件语句、循环或嵌套函数中调用
- 调用顺序必须保持一致
// ❌ 错误:条件语句中使用
function Component() {
if (condition) {
const [state, setState] = useState(0); // 错误!
}
}
// ✅ 正确:顶层使用
function Component() {
const [state, setState] = useState(0);
if (condition) {
// 在这里使用 state
}
}
Hooks 的存储结构
每个函数组件对应一个 Fiber 节点,Fiber 节点的 memoizedState 属性存储 Hooks 链表:
Hook 对象结构
interface Hook {
memoizedState: unknown; // 当前状态值
baseState: unknown; // 基础状态
baseQueue: Update | null; // 基础更新队列
queue: UpdateQueue | null; // 更新队列
next: Hook | null; // 指向下一个 Hook
}
Hooks 使用单向链表存储,每次渲染时按顺序遍历链表。这就是为什么 Hooks 必须在顶层调用、调用顺序必须一致——因为 React 是通过调用顺序来确定每个 Hook 对应的状态的。
useState 原理
简化实现
// 简化版 useState 实现
let workInProgressHook: Hook | null = null;
let currentHook: Hook | null = null;
let isMount = true; // 是否首次渲染
interface Hook {
memoizedState: unknown;
next: Hook | null;
queue: Array<(state: unknown) => unknown>;
}
function useState<T>(initialState: T): [T, (action: T | ((prev: T) => T)) => void] {
let hook: Hook;
if (isMount) {
// 首次渲染:创建新的 Hook
hook = {
memoizedState: typeof initialState === 'function'
? (initialState as () => T)()
: initialState,
next: null,
queue: [],
};
// 将 Hook 添加到链表
if (!workInProgressHook) {
fiber.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
} else {
// 更新渲染:复用已有的 Hook
hook = currentHook!;
currentHook = currentHook!.next;
// 处理更新队列
const queue = hook.queue;
let newState = hook.memoizedState;
queue.forEach(action => {
newState = typeof action === 'function'
? action(newState)
: action;
});
hook.memoizedState = newState;
hook.queue = [];
}
// 返回状态和更新函数
const setState = (action: T | ((prev: T) => T)) => {
hook.queue.push(action as (state: unknown) => unknown);
scheduleUpdate(); // 触发重新渲染
};
return [hook.memoizedState as T, setState];
}
执行流程
批量更新
function handleClick() {
setCount(count + 1); // 不会立即更新
setCount(count + 1); // 不会立即更新
setCount(count + 1); // 不会立即更新
// 只会触发一次重新渲染,最终 count + 1
}
// 正确的连续更新
function handleClick() {
setCount(prev => prev + 1); // 使用函数式更新
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// 最终 count + 3
}
useEffect 原理
简化实现
interface EffectHook extends Hook {
memoizedState: {
create: () => (() => void) | void; // effect 函数
destroy: (() => void) | void; // 清理函数
deps: unknown[] | null; // 依赖数组
};
}
function useEffect(
create: () => (() => void) | void,
deps?: unknown[]
): void {
let hook: EffectHook;
if (isMount) {
// 首次渲染:创建 Effect Hook
hook = {
memoizedState: {
create,
destroy: undefined,
deps: deps ?? null,
},
next: null,
};
// 将 effect 加入待执行队列
pushEffect(hook);
} else {
hook = currentHook as EffectHook;
currentHook = currentHook!.next;
const prevDeps = hook.memoizedState.deps;
// 比较依赖是否变化
if (deps && areHookInputsEqual(deps, prevDeps)) {
// 依赖未变化,跳过
return;
}
// 依赖变化,更新 effect
hook.memoizedState = { create, destroy: undefined, deps: deps ?? null };
pushEffect(hook);
}
}
// 浅比较依赖数组
function areHookInputsEqual(
nextDeps: unknown[],
prevDeps: unknown[] | null
): boolean {
if (prevDeps === null) return false;
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
执行时机
| Hook | 执行时机 | 是否阻塞渲染 |
|---|---|---|
useEffect | DOM 更新后,浏览器绑制后异步执行 | ❌ 不阻塞 |
useLayoutEffect | DOM 更新后,浏览器绑制前同步执行 | ✅ 阻塞 |
// useEffect:异步执行,不阻塞渲染
useEffect(() => {
// 发送网络请求等副作用
fetchData();
}, []);
// useLayoutEffect:同步执行,阻塞渲染
useLayoutEffect(() => {
// 需要在渲染前完成的 DOM 操作
const rect = elementRef.current.getBoundingClientRect();
setPosition(rect);
}, []);
Effect 生命周期
闭包陷阱
什么是闭包陷阱?
函数组件中的函数会捕获定义时的状态值,而不是最新的状态值。
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
// ❌ 闭包陷阱:count 是点击时的值,不是 3 秒后的值
console.log(count);
}, 3000);
};
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={handleClick}>Log after 3s</button>
</div>
);
}
闭包陷阱产生的原因
每次渲染都会创建新的函数,新函数捕获当次渲染的状态值。旧的 setTimeout 回调持有的仍是旧的函数闭包。
解决方案
方案 1:使用 useRef
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count); // 创建 ref
// 每次 count 变化时更新 ref
useEffect(() => {
countRef.current = count;
}, [count]);
const handleClick = () => {
setTimeout(() => {
// ✅ 正确:读取 ref 的最新值
console.log(countRef.current);
}, 3000);
};
return (/* ... */);
}
方案 2:使用函数式更新
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
// ✅ 使用函数式更新获取最新值
setCount(prevCount => {
console.log(prevCount); // 始终是最新值
return prevCount; // 不修改,只读取
});
}, 3000);
};
return (/* ... */);
}
方案 3:在 useEffect 中使用依赖
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
console.log(count); // 正确的 count
}, 3000);
return () => clearTimeout(timer);
}, [count]); // 依赖 count,每次变化重新设置定时器
return (/* ... */);
}
useEffect 中的闭包陷阱
// ❌ 错误:空依赖数组导致闭包陷阱
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1); // count 始终是 0
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖,effect 只执行一次
return <div>{count}</div>; // 始终显示 1
}
// ✅ 正确:使用函数式更新
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1); // 使用函数式更新
}, 1000);
return () => clearInterval(timer);
}, []);
return <div>{count}</div>; // 正常递增
}
自定义 Hook
自定义 Hook 的本质是复用状态逻辑:
// 自定义 Hook:追踪鼠标位置
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return position;
}
// 使用自定义 Hook
function Component() {
const { x, y } = useMousePosition();
return <div>Mouse: {x}, {y}</div>;
}
自定义 Hook 原理
每个组件调用自定义 Hook 时,Hook 内部的状态是独立的。自定义 Hook 只是复用逻辑,不共享状态。
常见面试问题
Q1: React Hooks 为什么不能在条件语句中使用?
答案:
Hooks 依赖调用顺序来确定每个 Hook 对应的状态:
// React 内部大致逻辑
// 第一次渲染
useState('A') // 第 1 个 Hook → 状态 A
useState('B') // 第 2 个 Hook → 状态 B
// 如果条件语句导致顺序变化
// 第二次渲染(假设条件不满足)
// 跳过了第一个 useState
useState('B') // 第 1 个 Hook → 错误地读取到状态 A!
React 使用链表按顺序存储 Hooks,每次渲染通过顺序匹配 Hook 和状态。条件语句会破坏顺序,导致状态错乱。
Q2: useState 是同步还是异步的?
答案:
useState 的更新是异步批量的(在 React 18 中):
function handleClick() {
setCount(count + 1);
console.log(count); // 旧值,不是更新后的值
setName('Alice');
setAge(25);
// 三次更新会被批量处理,只触发一次重新渲染
}
| React 版本 | 事件处理器中 | setTimeout/Promise 中 |
|---|---|---|
| React 17 | 批量更新 | 同步更新(每次 setState 触发一次渲染) |
| React 18 | 批量更新 | 自动批量更新 |
如果需要同步获取更新后的值,使用 flushSync:
import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => {
setCount(count + 1);
});
console.log(count); // 仍然是旧值!因为 count 是闭包
// 但 DOM 已经更新
}
Q3: useEffect 和 useLayoutEffect 的区别?
答案:
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | DOM 更新后,浏览器绑制后 | DOM 更新后,浏览器绑制前 |
| 是否阻塞 | 不阻塞渲染 | 阻塞渲染 |
| 使用场景 | 数据获取、订阅、日志 | DOM 测量、同步 DOM 操作 |
// useEffect:适合大多数场景
useEffect(() => {
fetchData();
}, []);
// useLayoutEffect:需要同步读取/修改 DOM
useLayoutEffect(() => {
// 读取 DOM 布局信息
const rect = ref.current.getBoundingClientRect();
// 同步更新位置,避免闪烁
ref.current.style.left = `${rect.width}px`;
}, []);
Q4: 如何解决 useEffect 中的闭包陷阱?
答案:
三种主要方案:
// 方案 1:添加依赖
useEffect(() => {
console.log(count);
}, [count]); // 依赖数组包含 count
// 方案 2:使用 useRef 存储最新值
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const timer = setInterval(() => {
console.log(countRef.current); // 始终是最新值
}, 1000);
return () => clearInterval(timer);
}, []);
// 方案 3:使用函数式更新
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1); // 不依赖外部 count
}, 1000);
return () => clearInterval(timer);
}, []);
Q5: useMemo 和 useCallback 的区别?
答案:
| Hook | 缓存内容 | 返回值 | 使用场景 |
|---|---|---|---|
useMemo | 计算结果 | 任意值 | 避免重复计算 |
useCallback | 函数本身 | 函数 | 避免函数重新创建 |
// useMemo:缓存计算结果
const expensiveValue = useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]);
// useCallback:缓存函数引用
const handleClick = useCallback(() => {
doSomething(a, b);
}, [a, b]);
// useCallback 等价于
const handleClick = useMemo(() => {
return () => doSomething(a, b);
}, [a, b]);
Q6: useEffect 的清除函数什么时候执行?闭包陷阱怎么解决?
答案:
useEffect 的清除函数(return 的函数)在以下两个时机执行:
- 组件卸载时:执行最后一次 effect 的清除函数
- 依赖变化导致 effect 重新执行前:先执行上一次 effect 的清除函数,再执行新的 effect
function ChatRoom({ roomId }: { roomId: string }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
console.log(`连接到房间 ${roomId}`);
// 清除函数:在下次 effect 执行前 或 组件卸载时调用
return () => {
connection.disconnect();
console.log(`断开房间 ${roomId}`);
};
}, [roomId]);
return <div>当前房间: {roomId}</div>;
}
// 假设 roomId 从 "A" 变为 "B",执行顺序:
// 1. 组件 render(roomId = "B")
// 2. DOM 更新
// 3. 执行上次的清除函数:断开房间 A
// 4. 执行新的 effect:连接到房间 B
清除函数捕获的是上一次渲染时的值,而不是最新值。这是符合预期的设计——清除函数需要清理的是上一次 effect 创建的副作用。
闭包陷阱的典型场景与解决方案:
// ❌ 闭包陷阱:count 始终是初始值 0
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // count 被闭包捕获,始终为 0
}, 1000);
return () => clearInterval(id);
}, []); // 空依赖 → effect 只执行一次 → count 永远是 0
return <span>{count}</span>; // 永远显示 1
}
// ✅ 方案 1:函数式更新(推荐,最简洁)
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1); // 不依赖外部闭包
}, 1000);
return () => clearInterval(id);
}, []);
// ✅ 方案 2:useRef 保存最新值
const countRef = useRef(count);
useEffect(() => {
countRef.current = count; // 每次渲染同步最新值
}, [count]);
useEffect(() => {
const id = setInterval(() => {
setCount(countRef.current + 1); // 通过 ref 读取最新值
}, 1000);
return () => clearInterval(id);
}, []);
// ✅ 方案 3:useReducer 替代(适合复杂状态逻辑)
const [count, dispatch] = useReducer((state: number, action: 'increment') => {
if (action === 'increment') return state + 1;
return state;
}, 0);
useEffect(() => {
const id = setInterval(() => {
dispatch('increment'); // dispatch 引用稳定,不需要依赖
}, 1000);
return () => clearInterval(id);
}, []);
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 函数式更新 | 状态更新只依赖前一个状态 | 简洁、无需额外依赖 | 无法访问其他状态 |
| useRef | 需要读取最新值但不触发重渲染 | 通用性强 | 多一层间接引用 |
| useReducer | 复杂状态逻辑 | dispatch 引用稳定 | 多写 reducer |
Q7: useMemo 和 useCallback 的区别?什么时候该用什么时候不该用?
答案:
useMemo 缓存计算结果,useCallback 缓存函数引用。useCallback(fn, deps) 本质上等价于 useMemo(() => fn, deps)。
// useMemo:缓存计算结果(值)
const sortedList = useMemo(() => {
return items.sort((a, b) => a.price - b.price); // 返回排序后的数组
}, [items]);
// useCallback:缓存函数本身(引用)
const handleDelete = useCallback((id: string) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []); // 函数引用在组件重渲染间保持不变
什么时候该用:
// ✅ 场景 1:传给 React.memo 子组件的回调函数
const MemoChild = React.memo(({ onClick }: { onClick: () => void }) => {
console.log('子组件渲染');
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// 不用 useCallback → 每次 Parent 渲染都创建新函数 → MemoChild 白白重渲染
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<>
<span>{count}</span>
<MemoChild onClick={handleClick} />
</>
);
}
// ✅ 场景 2:作为其他 Hook 的依赖
const fetchData = useCallback(async () => {
const res = await fetch(`/api/items?page=${page}`);
return res.json();
}, [page]);
useEffect(() => {
fetchData(); // fetchData 作为 useEffect 的依赖
}, [fetchData]);
// ✅ 场景 3:计算开销确实很大
const result = useMemo(() => {
return heavyComputation(data); // 真正的昂贵计算
}, [data]);
什么时候不该用:
// ❌ 简单计算,缓存的开销 > 重新计算
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
// ✅ 直接写
const fullName = `${firstName} ${lastName}`;
// ❌ 没有传给 memo 子组件的普通函数
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
// ✅ 直接写(没有子组件需要稳定引用)
const handleClick = () => setCount(c => c + 1);
// ❌ 每次都会变的依赖(缓存毫无意义)
const result = useMemo(() => format(data), [data]); // 如果 data 每次都是新对象
React 19 的 React Compiler 可以自动添加 memoization,未来在大多数场景下不需要手动写 useMemo / useCallback。但在 React 18 及以下版本,仍需手动优化。
| 判断标准 | 该用 | 不该用 |
|---|---|---|
| 计算成本 | 耗时 > 1ms 的计算 | 简单字符串拼接、基础运算 |
| 子组件 | 传给 React.memo 包裹的子组件 | 没有 memo 的子组件 |
| 依赖稳定性 | 作为其他 Hook 的依赖 | 只在 JSX 中使用 |
| 依赖变化频率 | 依赖不经常变化 | 依赖每次渲染都变 |
Q8: useRef 除了获取 DOM 还能做什么?
答案:
useRef 的本质是创建一个在整个组件生命周期内保持不变的可变容器对象 { current: T }。修改 .current 不会触发重新渲染,这使它有很多超越 DOM 引用的用途:
1. 存储可变值(跨渲染周期保持引用)
function Timer() {
const [count, setCount] = useState(0);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const start = () => {
timerRef.current = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
};
const stop = () => {
if (timerRef.current) {
clearInterval(timerRef.current); // 跨渲染访问同一个 timer ID
timerRef.current = null;
}
};
return (
<>
<span>{count}</span>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
</>
);
}
2. 保存上一次的状态值(usePrevious)
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value; // effect 在渲染后执行,此时 ref 保存的还是旧值
}, [value]);
return ref.current; // 返回上一次的值
}
// 使用
function PriceDisplay({ price }: { price: number }) {
const prevPrice = usePrevious(price);
const trend = prevPrice !== undefined && price > prevPrice ? '📈' : '📉';
return <span>{trend} {price}</span>;
}
3. 解决闭包陷阱(保存最新值)
function useLatest<T>(value: T) {
const ref = useRef(value);
ref.current = value; // 同步更新,确保始终是最新值
return ref;
}
function SearchBox() {
const [keyword, setKeyword] = useState('');
const latestKeyword = useLatest(keyword);
const handleSearch = useCallback(() => {
// 即使 useCallback 依赖为空,也能读到最新 keyword
fetch(`/api/search?q=${latestKeyword.current}`);
}, []); // 不需要依赖 keyword
return <input onChange={e => setKeyword(e.target.value)} />;
}
4. 标记组件是否已挂载(避免内存泄漏)
function useIsMounted() {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false; // 卸载时标记
};
}, []);
return isMounted;
}
function DataLoader() {
const [data, setData] = useState(null);
const isMounted = useIsMounted();
useEffect(() => {
fetchData().then(result => {
if (isMounted.current) { // 只在组件还挂载时更新状态
setData(result);
}
});
}, []);
return <div>{data}</div>;
}
5. 记录渲染次数(调试用)
function useRenderCount() {
const count = useRef(0);
count.current += 1; // 每次渲染都递增,但不触发重渲染
return count.current;
}
| 特性 | useRef | useState | 普通变量 |
|---|---|---|---|
| 修改是否触发重渲染 | 否 | 是 | 否 |
| 跨渲染周期保持值 | 是 | 是 | 否(每次渲染重新创建) |
| 可以同步读取最新值 | 是 | 否(闭包) | 否(重新创建) |
| 适用场景 | 可变引用、DOM | UI 状态 | 派生计算 |
Q9: 自定义 Hook 的设计原则和最佳实践?
答案:
自定义 Hook 是复用有状态逻辑的核心手段。以下是设计原则和实战示例:
设计原则:
- 以
use开头命名:这是 React 识别 Hook 的约定,ESLint 规则依赖此命名 - 单一职责:一个 Hook 只做一件事
- 返回值语义清晰:简单值返回单值/元组,复杂值返回对象
- 可组合:自定义 Hook 可以调用其他自定义 Hook
- 参数设计合理:必要参数在前,可选配置用对象
实战示例 1:useFetch
interface UseFetchOptions<T> {
immediate?: boolean; // 是否立即执行,默认 true
initialData?: T; // 初始数据
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
}
interface UseFetchReturn<T> {
data: T | undefined;
error: Error | null;
loading: boolean;
refresh: () => Promise<void>; // 手动重新请求
}
function useFetch<T>(
url: string,
options: UseFetchOptions<T> = {}
): UseFetchReturn<T> {
const { immediate = true, initialData, onSuccess, onError } = options;
const [data, setData] = useState<T | undefined>(initialData);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
// 用 useRef 保存最新的回调,避免依赖频繁变化
const onSuccessRef = useRef(onSuccess);
const onErrorRef = useRef(onError);
onSuccessRef.current = onSuccess;
onErrorRef.current = onError;
const refresh = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const json = await response.json() as T;
setData(json);
onSuccessRef.current?.(json);
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
onErrorRef.current?.(error);
} finally {
setLoading(false);
}
}, [url]); // 只依赖 url
useEffect(() => {
if (immediate) {
refresh();
}
}, [refresh, immediate]);
return { data, error, loading, refresh };
}
// 使用
function UserProfile({ userId }: { userId: string }) {
const { data: user, loading, error, refresh } = useFetch<User>(
`/api/users/${userId}`,
{
onSuccess: (user) => console.log('加载成功', user.name),
}
);
if (loading) return <Skeleton />;
if (error) return <ErrorMessage error={error} onRetry={refresh} />;
return <div>{user?.name}</div>;
}
实战示例 2:useDebounce
// 对值进行防抖:值变化后延迟更新
function useDebounce<T>(value: T, delay: number = 300): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer); // 值变化时清除上一个定时器
}, [value, delay]);
return debouncedValue;
}
// 对函数进行防抖:返回一个防抖后的函数
function useDebounceFn<T extends (...args: unknown[]) => unknown>(
fn: T,
delay: number = 300
) {
const fnRef = useRef(fn);
fnRef.current = fn; // 始终保持最新的函数引用
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const debouncedFn = useCallback((...args: Parameters<T>) => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
fnRef.current(...args);
}, delay);
}, [delay]);
// 组件卸载时清理
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);
return debouncedFn;
}
// 使用 useDebounce(值防抖)
function SearchPage() {
const [keyword, setKeyword] = useState('');
const debouncedKeyword = useDebounce(keyword, 500);
// debouncedKeyword 变化才触发请求
const { data } = useFetch<SearchResult>(
`/api/search?q=${debouncedKeyword}`
);
return <input value={keyword} onChange={e => setKeyword(e.target.value)} />;
}
- 命名:
use+ 动词/名词,清晰表达用途(useFetch、useDebounce、useLocalStorage) - 参数:必要参数直接传,可选参数用 options 对象,支持默认值
- 返回值:2 个以下用元组
[value, setter],3 个以上用对象{ data, loading, error } - 清理:在
useEffect的 return 中清理定时器、订阅等副作用 - 引用稳定性:回调参数用
useRef保存,返回的函数用useCallback包裹 - 泛型:让 Hook 支持多种数据类型,提高复用性
Q10: React 19 的 use Hook 是什么?和 useEffect 有什么区别?
答案:
use 是 React 19 新增的 Hook,用于在渲染期间读取 Promise 或 Context 的值。它是唯一一个可以在条件语句和循环中调用的 Hook。
import { use, Suspense } from 'react';
// use 可以直接在组件中读取 Promise
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // 渲染时读取,自动配合 Suspense
return <div>{user.name}</div>;
}
// 父组件
function App() {
const userPromise = fetchUser(userId); // 在渲染期间发起请求
return (
<Suspense fallback={<Loading />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
import { use } from 'react';
import { ThemeContext } from './ThemeContext';
function Button({ isSpecial }: { isSpecial: boolean }) {
// ✅ use 可以在条件语句中调用,useContext 不行!
if (isSpecial) {
const theme = use(ThemeContext);
return <button style={{ color: theme.primary }}>特殊按钮</button>;
}
return <button>普通按钮</button>;
}
use 和 useEffect 的核心区别:
| 特性 | use | useEffect |
|---|---|---|
| 执行时机 | 渲染期间(同步) | 渲染后(异步) |
| 用途 | 读取已有的 Promise/Context | 执行副作用(订阅、DOM 操作、请求) |
| 数据获取模式 | Promise 由父组件传入,配合 Suspense | 在 effect 中发起请求,管理 loading/error |
| 条件调用 | 可以在 if/for 中调用 | 不可以 |
| 清除函数 | 无 | 有(return cleanup) |
| 触发重渲染 | Promise resolve 后自动渲染 | 需要手动 setState |
// ❌ 传统方式:useEffect + useState(样板代码多)
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
fetchUser(userId)
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <Loading />;
if (error) return <ErrorView error={error} />;
return <div>{user?.name}</div>;
}
// ✅ React 19 方式:use + Suspense + ErrorBoundary(声明式)
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // 就这一行,loading/error 由外层处理
return <div>{user.name}</div>;
}
// 父组件
function App() {
const userPromise = fetchUser(userId);
return (
<ErrorBoundary fallback={<ErrorView />}>
<Suspense fallback={<Loading />}>
<UserProfile userPromise={userPromise} />
</Suspense>
</ErrorBoundary>
);
}
- Promise 必须由外部传入:不要在组件内部创建 Promise 再传给
use,否则每次渲染都会创建新的 Promise - 需要 Suspense 配合:
use读取 pending 的 Promise 时会"挂起"组件,必须有Suspense边界 - 不能替代所有 useEffect:
use只用于读取数据,订阅、DOM 操作等副作用仍需useEffect - 与 Server Components 配合最佳:Server Component 可以直接
await,Client Component 用use读取传入的 Promise
更多关于 React 19 的新特性,请参考 React 19 新特性。