React 高级 Hooks
本章将介绍 React 中的高级 Hooks,包括 useReducer、useLayoutEffect、useId、useTransition、useDeferredValue、useImperativeHandle 以及 React 19 新增的 Hooks。
useReducer:复杂状态管理
useReducer 是 useState 的替代方案,特别适合处理多个子状态或下一个状态依赖前一个状态的场景。它借鉴了 Redux 的设计思想,通过 action 来描述状态变化。
基本用法
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return initialState;
case 'set':
return { count: action.payload };
default:
throw new Error(`未知的 action 类型: ${action.type}`);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>计数: {state.count}</p>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'reset' })}>重置</button>
<button onClick={() => dispatch({ type: 'set', payload: 10 })}>设为10</button>
</div>
);
}
何时使用 useReducer
适合使用 useReducer 的场景:
- 状态逻辑复杂,包含多个子状态
- 下一个状态依赖于之前的状态
- 需要处理多种不同的状态变化方式
- 希望状态变化可预测、可测试
useState vs useReducer 对比:
| 特性 | useState | useReducer |
|---|---|---|
| 适用场景 | 简单状态 | 复杂状态逻辑 |
| 状态更新方式 | 直接设置新值 | 通过 action 描述变化 |
| 调试能力 | 一般 | 更好(可追踪 action) |
| 代码量 | 较少 | 较多 |
实际应用:表单状态管理
const formReducer = (state, action) => {
switch (action.type) {
case 'FIELD_CHANGE':
return {
...state,
values: { ...state.values, [action.field]: action.value },
errors: { ...state.errors, [action.field]: '' }
};
case 'SET_ERRORS':
return { ...state, errors: action.errors };
case 'SET_LOADING':
return { ...state, isLoading: action.isLoading };
case 'RESET':
return action.initialState;
default:
return state;
}
};
function ComplexForm() {
const initialState = {
values: { username: '', email: '', password: '' },
errors: {},
isLoading: false
};
const [state, dispatch] = useReducer(formReducer, initialState);
const handleChange = (field) => (e) => {
dispatch({ type: 'FIELD_CHANGE', field, value: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: 'SET_LOADING', isLoading: true });
try {
await submitForm(state.values);
dispatch({ type: 'RESET', initialState });
} catch (error) {
dispatch({ type: 'SET_ERRORS', errors: error.errors });
} finally {
dispatch({ type: 'SET_LOADING', isLoading: false });
}
};
// ...
}
useLayoutEffect:DOM 测量
useLayoutEffect 与 useEffect 类似,但它在浏览器绘制屏幕之前同步执行。这使它适合用于测量 DOM 布局并同步重新渲染,避免视觉闪烁。
useEffect vs useLayoutEffect
用户交互 → 状态更新 → React 渲染 → useLayoutEffect → 浏览器绘制 → useEffect
↑ ↑
同步执行 异步执行
使用场景:弹出菜单定位
function Tooltip({ content, children }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
const [isVisible, setIsVisible] = useState(false);
const triggerRef = useRef(null);
const tooltipRef = useRef(null);
useLayoutEffect(() => {
if (isVisible && triggerRef.current && tooltipRef.current) {
const triggerRect = triggerRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
// 计算 tooltip 位置,确保不超出视口
const top = triggerRect.top - tooltipRect.height - 8;
const left = triggerRect.left + (triggerRect.width - tooltipRect.width) / 2;
setPosition({
top: Math.max(0, top),
left: Math.max(0, Math.min(left, window.innerWidth - tooltipRect.width))
});
}
}, [isVisible]);
return (
<>
<span
ref={triggerRef}
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
>
{children}
</span>
{isVisible && (
<div
ref={tooltipRef}
style={{
position: 'fixed',
top: position.top,
left: position.left,
background: 'black',
color: 'white',
padding: '8px',
borderRadius: '4px'
}}
>
{content}
</div>
)}
</>
);
}
何时使用 useLayoutEffect
- 需要在绘制前测量 DOM 元素
- 需要在绘制前同步修改 DOM
- 防止视觉闪烁或布局抖动
useLayoutEffect 会阻塞浏览器绘制,应避免在其中执行耗时操作。
useId:生成唯一 ID
useId 用于生成唯一的 ID,主要用于无障碍属性(如 aria-labelledby)。
基本用法
function FormField({ label }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
);
}
function PasswordField() {
const id = useId();
return (
<>
<label htmlFor={id + '-password'}>密码:</label>
<input id={id + '-password'} type="password" />
<label htmlFor={id + '-show'}>显示密码</label>
<input id={id + '-show'} type="checkbox" />
</>
);
}
为什么不使用计数器或随机数?
- 服务端渲染兼容:
useId确保服务端和客户端生成相同的 ID - 避免冲突:同一组件在不同位置使用时 ID 不同
- 稳定性:组件重新渲染时 ID 保持不变
useTransition:非阻塞更新
useTransition(React 18+)允许将某些状态更新标记为"过渡",使其不阻塞用户界面交互。
基本概念
React 将状态更新分为两类:
- 紧急更新:直接响应用户输入(如打字、点击)
- 过渡更新:可以延迟(如列表过滤、搜索结果)
function SearchApp() {
const [isPending, startTransition] = useTransition();
const [input, setInput] = useState("");
const [list, setList] = useState([]);
function handleChange(e) {
const value = e.target.value;
// 紧急更新:立即更新输入框
setInput(value);
// 过渡更新:可以延迟更新列表
startTransition(() => {
const filtered = largeDataList.filter(item =>
item.name.includes(value)
);
setList(filtered);
});
}
return (
<div>
<input value={input} onChange={handleChange} />
{isPending ? (
<p>正在更新列表...</p>
) : (
<ul>
{list.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
)}
</div>
);
}
useTransition vs setTimeout
| 特性 | useTransition | setTimeout |
|---|---|---|
| 执行时机 | React 调度 | 延迟执行 |
| 可中断 | 是 | 否 |
| 性能优化 | React 自动处理 | 需手动处理 |
| 状态管理 | 内置 isPending | 需手动管理 |
useDeferredValue:延迟值
useDeferredValue 接收一个值,返回该值的"延迟版本"。当有更紧急的更新时,延迟版本会暂时保持旧值。
基本用法
function SearchResults({ query }) {
// 延迟的查询值
const deferredQuery = useDeferredValue(query);
// 使用延迟值渲染结果
const results = useMemo(
() => filterLargeList(deferredQuery),
[deferredQuery]
);
return (
<div>
{/* 输入框使用实时 query */}
{/* 结果列表使用延迟 query */}
<ul>
{results.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}
useTransition vs useDeferredValue
| 特性 | useTransition | useDeferredValue |
|---|---|---|
| 控制方式 | 包装更新代码 | 包装值 |
| 使用场景 | 控制状态更新时机 | 延迟特定值 |
| isPending | 有 | 无(需手动判断) |
useImperativeHandle:自定义 ref
useImperativeHandle 允许子组件向父组件暴露特定的方法,而不是整个 DOM 元素。
基本用法
// 子组件
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
// 只暴露特定方法给父组件
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = '';
},
getValue: () => {
return inputRef.current.value;
}
}));
return <input ref={inputRef} {...props} />;
});
// 父组件
function Form() {
const inputRef = useRef();
const handleFocus = () => {
inputRef.current.focus(); // 调用子组件暴露的方法
};
const handleClear = () => {
inputRef.current.clear();
};
return (
<>
<FancyInput ref={inputRef} />
<button onClick={handleFocus}>聚焦</button>
<button onClick={handleClear}>清空</button>
</>
);
}
实际应用:多步骤表单
const StepForm = forwardRef(({ step, onSubmit }, ref) => {
const formRef = useRef();
useImperativeHandle(ref, () => ({
validate: () => {
return formRef.current?.checkValidity() ?? false;
},
getData: () => {
return new FormData(formRef.current);
},
reset: () => {
formRef.current?.reset();
}
}));
return (
<form ref={formRef} onSubmit={onSubmit}>
{/* 表单内容 */}
</form>
);
});
function MultiStepForm() {
const [currentStep, setCurrentStep] = useState(0);
const formRefs = [useRef(), useRef(), useRef()];
const handleNext = () => {
if (formRefs[currentStep].current?.validate()) {
setCurrentStep(s => s + 1);
}
};
const handleSubmit = () => {
const allData = formRefs.map(ref => ref.current?.getData());
// 提交所有数据
};
// ...
}
React 19 新 Hooks
React 19 引入了多个新的 Hooks,用于简化常见的数据变更操作和表单处理。
useActionState
useActionState 用于处理表单提交和数据变更操作,自动处理待定状态和错误状态。
import { useActionState } from 'react';
function UpdateNameForm() {
const [error, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const name = formData.get('name');
// 调用 API 更新名字
const error = await updateName(name);
if (error) {
return error; // 返回错误信息作为 state
}
// 成功后跳转
redirect('/profile');
return null;
},
null // 初始状态
);
return (
<form action={submitAction}>
<input type="text" name="name" required />
<button type="submit" disabled={isPending}>
{isPending ? '更新中...' : '更新名字'}
</button>
{error && <p className="error">{error}</p>}
</form>
);
}
参数说明:
- 第一个参数:Action 函数,接收
(previousState, payload)参数 - 第二个参数:初始状态值
- 第三个参数(可选):用于设置 action 的 permalink(URL)
返回值:
state:Action 返回的最后结果dispatch:用于触发 action 的函数isPending:是否处于待定状态
useOptimistic
useOptimistic 用于实现乐观更新,在异步操作完成前就显示预期结果,提升用户体验。
import { useOptimistic } from 'react';
function TodoList({ todos, onUpdateTodo }) {
const [optimisticTodos, setOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => {
// 乐观更新:立即显示新状态
return state.map(todo =>
todo.id === newTodo.id ? { ...todo, ...newTodo } : todo
);
}
);
async function updateTodo(id, completed) {
// 立即显示乐观更新
setOptimisticTodo({ id, completed });
// 发送请求
await updateTodoAPI(id, completed);
// 成功后更新实际数据
onUpdateTodo(id, completed);
}
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.id === updatingId ? 0.6 : 1 }}>
<input
type="checkbox"
checked={todo.completed}
onChange={(e) => updateTodo(todo.id, e.target.checked)}
/>
{todo.title}
</li>
))}
</ul>
);
}
典型使用场景:
- 点赞/收藏操作
- 待办事项状态切换
- 评论发送
- 购物车数量修改
use API
use 是 React 19 新增的 API,用于在渲染过程中读取资源(Promise 或 Context)。它是唯一可以在条件语句中使用的 Hook。
读取 Promise
import { use, Suspense } from 'react';
function Comments({ commentsPromise }) {
// use 会暂停直到 Promise 解析完成
const comments = use(commentsPromise);
return (
<ul>
{comments.map(comment => (
<li key={comment.id}>{comment.text}</li>
))}
</ul>
);
}
function Post({ postPromise }) {
return (
<Suspense fallback={<div>加载评论中...</div>}>
<Comments commentsPromise={postPromise.comments} />
</Suspense>
);
}
读取 Context
import { use } from 'react';
import ThemeContext from './ThemeContext';
function Heading({ children }) {
if (children == null) {
return null;
}
// use 可以在条件语句中调用(与 useContext 不同)
const theme = use(ThemeContext);
return (
<h1 style={{ color: theme.color }}>
{children}
</h1>
);
}
use 与 useContext 的区别:
| 特性 | useContext | use |
|---|---|---|
| 调用位置 | 组件顶层 | 任意位置 |
| 条件调用 | 不支持 | 支持 |
| 读取 Promise | 不支持 | 支持 |
useFormStatus
useFormStatus 用于获取父级 <form> 的状态,常用于设计系统中的按钮组件。
import { useFormStatus } from 'react-dom';
function SubmitButton({ children }) {
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '提交中...' : children}
</button>
);
}
function ContactForm() {
async function handleSubmit(formData) {
await sendEmail(formData);
}
return (
<form action={handleSubmit}>
<input name="email" type="email" required />
<textarea name="message" required />
<SubmitButton>发送消息</SubmitButton>
</form>
);
}
返回值说明:
pending:布尔值,表单是否正在提交data:FormData 对象,包含表单数据method:表单方法('get' 或 'post')action:表单的 action 属性值
React 19 其他改进
ref 作为 prop
React 19 中,ref 可以直接作为 prop 传递,不再需要 forwardRef:
// React 19 新写法 - 无需 forwardRef
function Input({ placeholder, ref }) {
return <input placeholder={placeholder} ref={ref} />;
}
// 使用
function App() {
const inputRef = useRef();
return <Input ref={inputRef} placeholder="输入内容" />;
}
Context 简化
React 19 中可以直接使用 <Context> 作为提供者:
const ThemeContext = createContext('light');
// React 19 新写法
function App({ children }) {
return (
<ThemeContext value="dark">
{children}
</ThemeContext>
);
}
// 旧写法仍然支持
<ThemeContext.Provider value="dark">
{children}
</ThemeContext.Provider>
ref 清理函数
React 19 支持在 ref 回调中返回清理函数:
function VideoPlayer() {
return (
<video
ref={(ref) => {
if (ref) {
ref.play();
ref.addEventListener('timeupdate', handleTimeUpdate);
}
// 新特性:返回清理函数
return () => {
if (ref) {
ref.removeEventListener('timeupdate', handleTimeUpdate);
}
};
}}
/>
);
}
文档元数据支持
React 19 支持在组件中渲染 <title>、<meta>、<link> 标签,它们会被自动提升到 <head> 中:
function BlogPost({ post }) {
return (
<article>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<meta name="keywords" content={post.keywords} />
<link rel="canonical" href={`https://example.com/posts/${post.id}`} />
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
完整示例:React 19 表单
以下是一个使用 React 19 新特性的完整表单示例:
import { useActionState, useOptimistic, useRef } from 'react';
import { useFormStatus } from 'react-dom';
// 提交按钮组件
function SubmitButton({ children }) {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '保存中...' : children}
</button>
);
}
// 用户资料表单
function UserProfile({ user, onUpdateUser }) {
const [optimisticUser, setOptimisticUser] = useOptimistic(user);
const [result, formAction] = useActionState(
async (prevState, formData) => {
const updates = {
name: formData.get('name'),
email: formData.get('email'),
};
// 乐观更新
setOptimisticUser(updates);
try {
await updateUser(user.id, updates);
onUpdateUser(updates);
return { success: true, message: '保存成功!' };
} catch (err) {
return { success: false, error: err.message };
}
},
null
);
return (
<form action={formAction}>
<div>
<label>用户名:</label>
<input
name="name"
defaultValue={optimisticUser.name}
required
/>
</div>
<div>
<label>邮箱:</label>
<input
name="email"
type="email"
defaultValue={optimisticUser.email}
required
/>
</div>
<SubmitButton>保存</SubmitButton>
{result?.success && (
<p className="success">{result.message}</p>
)}
{result?.error && (
<p className="error">{result.error}</p>
)}
</form>
);
}
Hooks 使用规则
在使用所有 Hooks 时,必须遵循以下规则:
1. 只在顶层调用
// ❌ 错误:在条件语句中调用
if (condition) {
const [value, setValue] = useState(0);
}
// ❌ 错误:在循环中调用
for (let i = 0; i < items.length; i++) {
const [value, setValue] = useState(items[i]);
}
// ✅ 正确:在组件顶层调用
function Component() {
const [value, setValue] = useState(0);
// ...
}
2. 只在 React 函数中调用
// ❌ 错误:在普通函数中调用
function handleClick() {
const [value, setValue] = useState(0); // 错误!
}
// ✅ 正确:在组件或自定义 Hook 中调用
function useCustomHook() {
const [value, setValue] = useState(0);
return value;
}
3. use API 的特殊规则
use API 是例外,它可以在条件语句中调用:
function Component({ condition, promise, context }) {
if (condition) {
// ✅ use 可以在条件中调用
const value = use(promise);
const theme = use(context);
}
}
小结
本章我们学习了 React 中的高级 Hooks:
- useReducer:适合管理复杂状态逻辑,通过 action 描述状态更新
- useLayoutEffect:在浏览器绘制前执行,用于 DOM 测量和避免闪烁
- useId:生成唯一 ID,用于无障碍属性
- useTransition:将状态更新标记为非阻塞,保持 UI 响应
- useDeferredValue:延迟值的更新,优化性能
- useImperativeHandle:自定义暴露给父组件的 ref 方法
- useActionState(React 19):处理表单提交和数据变更
- useOptimistic(React 19):实现乐观更新
- use(React 19):在渲染中读取 Promise 或 Context
- useFormStatus(React 19):获取表单状态
练习
- 使用
useReducer实现一个完整的待办事项列表(增删改查) - 使用
useLayoutEffect实现一个自适应位置的弹出菜单 - 使用
useId创建一个包含多个表单字段的组件 - 使用
useTransition优化搜索列表的输入体验 - 使用 React 19 的
useActionState和useOptimistic实现一个点赞功能