跳到主要内容

Hooks 原理

问题

React Hooks 是如何实现的?useState 和 useEffect 的原理是什么?什么是闭包陷阱?

答案

Hooks 是 React 16.8 引入的特性,让函数组件也能拥有状态和生命周期。Hooks 的核心实现依赖于 Fiber 架构链表结构

Hooks 的基本规则

在理解原理之前,先了解 Hooks 的使用规则:

Hooks 规则
  1. 只能在函数组件或自定义 Hook 中使用
  2. 只能在顶层调用,不能在条件语句、循环或嵌套函数中调用
  3. 调用顺序必须保持一致
// ❌ 错误:条件语句中使用
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执行时机是否阻塞渲染
useEffectDOM 更新后,浏览器绑制后异步执行❌ 不阻塞
useLayoutEffectDOM 更新后,浏览器绑制前同步执行✅ 阻塞
// 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 的区别?

答案

特性useEffectuseLayoutEffect
执行时机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 的函数)在以下两个时机执行:

  1. 组件卸载时:执行最后一次 effect 的清除函数
  2. 依赖变化导致 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 创建的副作用。

闭包陷阱的典型场景与解决方案

场景:setInterval 中的闭包陷阱
// ❌ 闭包陷阱: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 Compiler 的影响

React 19 的 React Compiler 可以自动添加 memoization,未来在大多数场景下不需要手动写 useMemo / useCallback。但在 React 18 及以下版本,仍需手动优化。

判断标准该用不该用
计算成本耗时 > 1ms 的计算简单字符串拼接、基础运算
子组件传给 React.memo 包裹的子组件没有 memo 的子组件
依赖稳定性作为其他 Hook 的依赖只在 JSX 中使用
依赖变化频率依赖不经常变化依赖每次渲染都变

Q8: useRef 除了获取 DOM 还能做什么?

答案

useRef 的本质是创建一个在整个组件生命周期内保持不变的可变容器对象 { current: T }。修改 .current 不会触发重新渲染,这使它有很多超越 DOM 引用的用途:

1. 存储可变值(跨渲染周期保持引用)

存储定时器 ID
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)

自定义 usePrevious Hook
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. 解决闭包陷阱(保存最新值)

useLatest Hook
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 vs useState vs 普通变量
特性useRefuseState普通变量
修改是否触发重渲染
跨渲染周期保持值否(每次渲染重新创建)
可以同步读取最新值否(闭包)否(重新创建)
适用场景可变引用、DOMUI 状态派生计算

Q9: 自定义 Hook 的设计原则和最佳实践?

答案

自定义 Hook 是复用有状态逻辑的核心手段。以下是设计原则和实战示例:

设计原则

  1. use 开头命名:这是 React 识别 Hook 的约定,ESLint 规则依赖此命名
  2. 单一职责:一个 Hook 只做一件事
  3. 返回值语义清晰:简单值返回单值/元组,复杂值返回对象
  4. 可组合:自定义 Hook 可以调用其他自定义 Hook
  5. 参数设计合理:必要参数在前,可选配置用对象

实战示例 1:useFetch

hooks/useFetch.ts
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

hooks/useDebounce.ts
// 对值进行防抖:值变化后延迟更新
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)} />;
}
自定义 Hook 设计清单
  1. 命名use + 动词/名词,清晰表达用途(useFetchuseDebounceuseLocalStorage
  2. 参数:必要参数直接传,可选参数用 options 对象,支持默认值
  3. 返回值:2 个以下用元组 [value, setter],3 个以上用对象 { data, loading, error }
  4. 清理:在 useEffect 的 return 中清理定时器、订阅等副作用
  5. 引用稳定性:回调参数用 useRef 保存,返回的函数用 useCallback 包裹
  6. 泛型:让 Hook 支持多种数据类型,提高复用性

Q10: React 19 的 use Hook 是什么?和 useEffect 有什么区别?

答案

use 是 React 19 新增的 Hook,用于在渲染期间读取 Promise 或 Context 的值。它是唯一一个可以在条件语句和循环中调用的 Hook。

use 读取 Promise
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>
);
}
use 读取 Context(可条件调用)
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 的核心区别

特性useuseEffect
执行时机渲染期间(同步)渲染后(异步)
用途读取已有的 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>
);
}
use 的注意事项
  1. Promise 必须由外部传入:不要在组件内部创建 Promise 再传给 use,否则每次渲染都会创建新的 Promise
  2. 需要 Suspense 配合use 读取 pending 的 Promise 时会"挂起"组件,必须有 Suspense 边界
  3. 不能替代所有 useEffectuse 只用于读取数据,订阅、DOM 操作等副作用仍需 useEffect
  4. 与 Server Components 配合最佳:Server Component 可以直接 await,Client Component 用 use 读取传入的 Promise

更多关于 React 19 的新特性,请参考 React 19 新特性

相关链接