跳到主要内容

React 高级 Hooks

本章将介绍 React 中的高级 Hooks,包括 useReduceruseLayoutEffectuseIduseTransitionuseDeferredValueuseImperativeHandle 以及 React 19 新增的 Hooks。


useReducer:复杂状态管理

useReduceruseState 的替代方案,特别适合处理多个子状态或下一个状态依赖前一个状态的场景。它借鉴了 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 对比

特性useStateuseReducer
适用场景简单状态复杂状态逻辑
状态更新方式直接设置新值通过 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 测量

useLayoutEffectuseEffect 类似,但它在浏览器绘制屏幕之前同步执行。这使它适合用于测量 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 将状态更新分为两类:

  1. 紧急更新:直接响应用户输入(如打字、点击)
  2. 过渡更新:可以延迟(如列表过滤、搜索结果)
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

特性useTransitionsetTimeout
执行时机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

特性useTransitionuseDeferredValue
控制方式包装更新代码包装值
使用场景控制状态更新时机延迟特定值
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 的区别

特性useContextuse
调用位置组件顶层任意位置
条件调用不支持支持
读取 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:

  1. useReducer:适合管理复杂状态逻辑,通过 action 描述状态更新
  2. useLayoutEffect:在浏览器绘制前执行,用于 DOM 测量和避免闪烁
  3. useId:生成唯一 ID,用于无障碍属性
  4. useTransition:将状态更新标记为非阻塞,保持 UI 响应
  5. useDeferredValue:延迟值的更新,优化性能
  6. useImperativeHandle:自定义暴露给父组件的 ref 方法
  7. useActionState(React 19):处理表单提交和数据变更
  8. useOptimistic(React 19):实现乐观更新
  9. use(React 19):在渲染中读取 Promise 或 Context
  10. useFormStatus(React 19):获取表单状态

练习

  1. 使用 useReducer 实现一个完整的待办事项列表(增删改查)
  2. 使用 useLayoutEffect 实现一个自适应位置的弹出菜单
  3. 使用 useId 创建一个包含多个表单字段的组件
  4. 使用 useTransition 优化搜索列表的输入体验
  5. 使用 React 19 的 useActionStateuseOptimistic 实现一个点赞功能

参考资料