
一句话:React 19 围绕「表单与异步」补了一组 Hooks——
use(在渲染期读 Promise / Context)、useActionState(管理提交结果与 pending)、useFormStatus(子组件读提交态)、useOptimistic(自动回滚的乐观更新);它们与 Server Actions 一起,把「提交 → loading → 错误 → 乐观更新 → 刷新」这条链路标准化了。
一、总览:新增了什么、各自解决什么
| Hook / 能力 | 解决的问题 | 关键限制 |
|---|---|---|
use(promise \| context) |
在渲染期读取「未就绪的值」(挂起到最近的 Suspense) | 必须在渲染期调用;Promise 必须稳定;不能放 try/catch |
useActionState(action, initial, permalink?) |
把「Action 的返回值」与「pending 状态」交给 React 管 | 只对 Action(表单提交 / transition 内的异步函数)有意义 |
useFormStatus() |
子组件读取「所属 <form> 的提交状态」 |
必须在 <form> 的子组件里调用(同级读不到) |
useOptimistic(state, updateFn) |
乐观更新(失败自动回滚) | 更新函数必须在 Action / transition 中调用 |
<form action={fn}> |
提交时自动调 fn(formData),自动 preventDefault + 重置 |
fn 必须是 Action(普通函数或 Server Action) |
useFormState → useActionState |
v19 改名(旧名仍可导入但已废弃) | 迁移时改 import 名即可 |
Server Actions('use server') |
表单可直接调用服务端函数 | 它是公开 HTTP 端点,必须自己鉴权 + 校验 |
⭐ 一句话抓住这组 API 的分工:
谁提交? →
二、use:在渲染期读取 Promise 与 Context
2.1 读 Promise(配合 Suspense)
import { use, Suspense } from 'react';
function Comments({ commentsPromise }: { commentsPromise: Promise }) {
const comments = use(commentsPromise); // ⭐ 未就绪时「挂起」到最近的 Suspense
return (
{comments.map((c) => - {c.text}
)}
);
}
function Page() {
// ⚠️ 关键:Promise 必须「稳定」——不能在渲染中创建
const [commentsPromise] = useState(() => fetchComments()); // ✅ 只创建一次
return (
}>
);
}
⭐⭐ 关于「Promise 必须稳定」的完整解释(这是最容易踩的坑):
❌ 在渲染中创建 Promise:
const comments = use(fetchComments());
→ 每次渲染都会「新建一个未完成的 Promise」
→ React 每次渲染都会「挂起」→ 【无限挂起】(页面永远显示 fallback)
✅ 三种让 Promise 稳定的方式:
① 由「上层/父组件」创建后作为 prop 传下来(React 官方例子的做法)
② 用 useState 惰性初始化:const [p] = useState(() => fetchComments())
③ 由「缓存」提供:cache() / TanStack Query / Server Component 预取
✅ 更推荐的第 ④ 种(Next.js App Router 的官方写法):
在 Server Component 里「先启动请求、把 Promise 传下去」:
const commentsPromise = fetchComments(); // 服务端启动,不 await
return ; // 传给客户端组件
→ Promise 在服务端创建一次,客户端组件用 use() 消费
2.2 读 Context(与 useContext 的关键差异)
import { use, createContext } from 'react';
const ThemeContext = createContext<'light' | 'dark'>('light');
// ✅ use 可以在「条件 / 循环」里调用(useContext 不行)
function Item({ items }: { items: Item[] }) {
return (
{items.map((item) => {
const theme = use(ThemeContext); // ⭐ 在循环里读 Context —— 合法!
return - {item.name}
;
})}
);
}
function Panel({ showTheme }: { showTheme: boolean }) {
if (showTheme) {
const theme = use(ThemeContext); // ⭐ 在条件里读 —— 合法!
return {theme};
}
return 无主题;
}
| 维度 | useContext(Ctx) |
use(Ctx) |
|---|---|---|
| 能否在条件 / 循环里调用 | ❌(Hook 规则) | ✅ |
| 能否在普通函数里调用 | ❌ | ⚠️ 可以,但仍必须发生在渲染期间 |
| 能否读 Promise | ❌ | ✅ |
| 订阅粒度 | 整个 value |
同样是整个 value(没有变细) |
| 是否废弃 | ❌ 未废弃 | — |
⚠️ `use(Ctx)` 并没有改变 Context 的「订阅粒度」——
它只是「调用位置更自由」。
「value 一变,所有消费者都重渲染」这条规律依然成立(见 3.4 / 12.8)。
⚠️ `use` 仍然**只能在渲染期间调用**:
❌ 事件处理函数里
❌ useEffect 里
❌ 普通工具函数里(除非它就是渲染过程的一部分)
2.3 use 的三条硬性约束
// ① ❌ 不能在 try/catch 里调用
function Bad() {
try {
const data = use(promise); // ❌ use 会「抛出 Promise」来实现挂起,
} catch { // try/catch 会把它捕获 → Suspense 失效
return ;
}
}
// ✅ 正确做法:把错误交给 ErrorBoundary
}>
}>
{/* use 的行为交给这两个边界 */}
// ② ❌ 不能在组件外部调用
const data = use(promise); // ❌ 不在组件/Hook 里
// ③ ✅ 可以在「渲染期间的任何位置」调用(含条件、循环、自定义 Hook)
function useTheme() {
return use(ThemeContext); // ✅ 自定义 Hook 里也合法
}
⭐ 为什么「不能放 try/catch」?(面试会追问)
因为 `use` 的挂起机制就是「抛出 Promise」——
React 在渲染时捕获这个 Promise,并去「最近的 Suspense」渲染 fallback。
如果外面包了 try/catch,Promise 会被你自己捕获,
React 就看不到它 → 不会挂起 → 你会拿到一个 Promise 对象而不是数据,
于是「渲染出 [object Promise]」或直接报错。
⭐ 那「错误」怎么办?
use 只负责「等待」,不负责「错误」——
Promise reject 时的错误会被【抛出】并由 【ErrorBoundary】接管。
所以标准组合依然是「ErrorBoundary 在外、Suspense 在内」(见 8.4)。
2.4 服务端组件里不需要 use
// ✅ Server Component 可以直接 await(不需要 use、不需要 Suspense)
export default async function Page() {
const comments = await fetchComments(); // ⭐ 直接 await
return {comments.map((c) => - {c.text}
)}
;
}
// ⚠️ 但这样是「串行」的:必须等 fetchComments 完成才开始渲染
// ✅ 想「并行 + 流式」就用 Suspense 包住「慢的那块」:
export default function Page() {
return (
<>
}>
{/* Comments 自己 async */}
>
);
}
async function Comments() {
const comments = await fetchComments(); // 慢
return ;
}
| 场景 | 怎么写 |
|---|---|
| Server Component 取数据 | 直接 await(不需要 use) |
| Client Component 消费「服务端启动的 Promise」 | use(promise)(配合 Suspense) |
| Client Component 读 Context | use(Ctx) 或 useContext(Ctx) |
| 数据获取(客户端发起) | TanStack Query 更合适(缓存/重试/失效,见 7.4) |
三、useActionState:管理提交结果与 pending
3.1 签名
const [state, formAction, isPending] = useActionState(actionFn, initialState, permalink?);
| 返回值 | 含义 |
|---|---|
state |
actionFn 的返回值(初始为 initialState) |
formAction |
可直接传给 <form action={formAction}> 或 <button formAction={formAction}> |
isPending |
是否有「正在执行的 Action」 |
// actionFn 的签名:接收「上一次的 state」与「FormData」
type ActionFn = (prevState: S, formData: FormData) => S | Promise;
3.2 基础用法
import { useActionState } from 'react';
interface SignupState {
error?: string;
fieldErrors?: Record;
success?: boolean;
}
async function signupAction(prevState: SignupState, formData: FormData): Promise {
const email = String(formData.get('email') ?? '');
const password = String(formData.get('password') ?? '');
// ⭐ 服务端也要校验(不能只信前端)
const fieldErrors: Record = {};
if (!email) fieldErrors.email = '请输入邮箱';
if (password.length < 8) fieldErrors.password = '密码至少 8 位';
if (Object.keys(fieldErrors).length) return { ...prevState, fieldErrors };
try {
await api.signup({ email, password });
return { success: true };
} catch (err) {
return { ...prevState, error: err instanceof Error ? err.message : '注册失败' };
}
}
function SignupForm() {
const [state, formAction, isPending] = useActionState(signupAction, {});
if (state.success) return 注册成功,请查收验证邮件
;
return (
);
}
⭐ 它相比「手写 useState + onSubmit」省掉了什么?
手写版(React 18 之前):
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [fieldErrors, setFieldErrors] = useState({});
const handleSubmit = async (e) => {
e.preventDefault(); // ① 手动阻止默认
setLoading(true);
const fd = new FormData(e.currentTarget); // ② 手动收集
try { ... } catch { ... }
finally { setLoading(false); } // ③ 手动管理 loading
};
useActionState 版:
· ①② 由 React 处理(写在 actionFn 里,直接拿 FormData)
· ③ 由 isPending 提供
· 结果状态由 state 统一管理(含「上一次的返回值」)
· ⭐ 还有一点:Action 结束后 React 会【自动重置非受控表单】
3.3 permalink:渐进增强
// 第三个参数 permalink:当「JavaScript 还没加载完」时,
// 表单会以【原生方式】提交到这个 URL(由服务端处理)
const [state, formAction, isPending] = useActionState(signupAction, {}, '/signup');
// ⭐ 场景:用户网络慢,JS 还没到就点了提交 —— 依然能工作(不用等 hydration)
// ⚠️ 它需要「服务端也实现了同样的提交逻辑」(渐进增强)
3.4 与「多个表单 / 多按钮」的配合
// ① 一个页面里多个独立表单 → 每个表单一个 useActionState
function Page() {
return (
<>
{/* 各自持有自己的 useActionState */}
>
);
}
// ② 同一个表单「多个提交按钮」→ 用 formAction 覆盖
function EditPost({ save, publish }: { save: ActionFn; publish: ActionFn }) {
const [saveState, saveAction] = useActionState(save, {});
const [publishState, publishAction] = useActionState(publish, {});
return (
);
}
// ③ 用「隐藏字段」区分「点了哪个按钮」
function Form() {
const [state, formAction] = useActionState(async (prev, fd) => {
const intent = fd.get('intent'); // 'save' | 'delete'
return intent === 'delete' ? await remove(fd) : await save(fd);
}, {});
return (
);
}
四、useFormStatus:子组件读取提交状态
4.1 签名与返回值
import { useFormStatus } from 'react-dom'; // ⚠️ 注意:来自 react-dom,不是 react
const { pending, data, method, action } = useFormStatus();
| 字段 | 类型 | 含义 |
|---|---|---|
pending |
boolean |
所属表单是否正在提交 |
data |
FormData \| null |
正在提交的数据(可用于「显示提交中显示什么」) |
method |
'get' \| 'post' |
提交方法 |
action |
string \| function \| null |
提交目标 |
4.2 核心约束:必须在 <form> 的子组件里
// ❌ 错误:和 form 在同一个组件里
function BadForm() {
const { pending } = useFormStatus(); // ⚠️ 读到的是「外层的 form」(通常是空/undefined)
return (
);
}
// ✅ 正确:抽成子组件
function SubmitButton({ children }: { children: React.ReactNode }) {
const { pending } = useFormStatus(); // ⭐ 读到「最近的 form」的状态
return ;
}
function Form() {
return (
);
}
⭐ 为什么「必须在子组件里」?
因为 React 是通过「**表单状态 Context**」把 pending 传下去的——
这个 Provider 由 `
4.3 能做的不止「禁用按钮」
// ① 提交中禁用整个表单的输入(避免中途修改)
function FormFields() {
const { pending } = useFormStatus();
return (
);
}
// ② 读取「正在提交的数据」(做「乐观显示」或「按内容显示不同提示」)
function OptimisticPreview() {
const { pending, data } = useFormStatus();
const title = data?.get('title');
return pending && title ? 正在发布「{title}」…
: null;
}
// ③ 根据「提交方法」显示不同文案
function SubmitLabel() {
const { pending, method } = useFormStatus();
if (!pending) return <>保存>;
return <>{method === 'get' ? '查询中…' : '保存中…'}>;
}
4.4 嵌套表单 / 多个表单
function App() {
return (
);
}
// ⭐ 规则与 Context 一致:读「最近的祖先 form」
// ⚠️ 但 HTML 规范不允许
五、useOptimistic:自动回滚的乐观更新
5.1 语义:它是「叠加」,不是「状态」
const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);
// state —— 「真实数据」(通常来自服务端/父组件)
// updateFn —— (currentState, optimisticValue) => 新的乐观状态
// optimisticState —— 「真实数据 + 尚未落地的乐观修改」(临时)
// addOptimistic —— 添加一次乐观更新(必须在 Action / transition 中调用)
⭐⭐ 最关键的理解:它没有「回滚代码」
useOptimistic 的乐观值【生命周期 = 一个 Action】:
① Action 开始 → addOptimistic(v) 把 v「叠加」到 state 上 → UI 立刻变
② Action 结束(成功 或 失败)→ 乐观叠加【被丢弃】→ UI 回到 state 的渲染结果
③ 如果 Action 成功且服务端数据已更新(如 revalidate),UI 显示新数据
如果 Action 失败,UI 自动回到「原样」—— 这就是「自动回滚」
✅ 所以它不需要你写「previousValue 保存 + onError 回滚」那套
⚠️ 但也意味着「你不能控制回滚时机」(Action 结束就回滚)
5.2 基础用法:评论列表
'use client';
import { useOptimistic, useRef } from 'react';
interface Comment {
id: string;
text: string;
pending?: boolean;
}
export function CommentSection({
comments,
addComment,
}: {
comments: Comment[];
addComment: (text: string) => Promise;
}) {
const formRef = useRef(null);
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(current: Comment[], newText: string) => [
...current,
{ id: `temp-${crypto.randomUUID()}`, text: newText, pending: true }, // ⭐ 临时项
]
);
async function formAction(formData: FormData) {
const text = String(formData.get('text') ?? '').trim();
if (!text) return;
addOptimisticComment(text); // ⭐ 立刻显示(必须在 Action 中调用)
formRef.current?.reset(); // 手动清空输入框
await addComment(text); // 真实提交(失败则乐观值自动丢弃)
}
return (
{optimisticComments.map((c) => (
- {/* ⭐ 临时项半透明 */}
{c.text}
))}
);
}
5.3 四种常见形态(覆盖增 / 删 / 改 / 赞)
// ① 【新增】:追加一个带临时 id 的项
const [optimistic, addOptimistic] = useOptimistic(items, (cur, text: string) => [
...cur,
{ id: `temp-${crypto.randomUUID()}`, text, pending: true },
]);
// ② 【删除】:把目标项「标记为删除中」或直接过滤掉
const [optimistic, markDeleting] = useOptimistic(items, (cur, id: string) =>
cur.map((it) => (it.id === id ? { ...it, deleting: true } : it)) // ⭐ 标记而不是移除(体验更好)
);
// 或直接移除(更「乐观」但失败时会有「跳回来」的突兀感):
// const [optimistic, remove] = useOptimistic(items, (cur, id: string) => cur.filter((it) => it.id !== id));
// ③ 【修改】:就地替换某一项
const [optimistic, patch] = useOptimistic(items, (cur, p: { id: string; text: string }) =>
cur.map((it) => (it.id === p.id ? { ...it, ...p, pending: true } : it))
);
// ④ 【点赞】:基于「当前乐观值」计算(保证连续点击也能正确累加)
const [optimistic, toggleLike] = useOptimistic(post, (cur, _: void) => ({
...cur,
liked: !cur.liked,
likeCount: cur.likeCount + (cur.liked ? -1 : 1),
}));
// ⚠️ 注意:updateFn 的 current 是「真实数据」,不是「上一次乐观值」
// → 连续多次 addOptimistic 时,每次都基于「同一个真实数据」计算
// → 想要「叠加」效果,必须让 updateFn 接收「上一次乐观结果」——
// 而实际做法是「把上一次的乐观结果也算进 current」(见下方「多次叠加」说明)
⭐⭐ 「多次叠加」的正确理解(这是最容易搞错的地方)
React 文档里的 updateFn 签名是 (currentState, optimisticValue) => ...
其中 currentState 是「**当前的真实状态**」(不是上一次乐观值)。
那「连续两次 addOptimistic」会怎样?
React 会把「多次 add」合成「一串 reducer 应用」——
实际效果是「第 2 次看到的 current 已经包含第 1 次的乐观结果」。
(这与 useReducer 的「队列依次应用」语义一致)
⚠️ 所以:
· 如果你写的是「追加」,连续两次会追加两项(正确)
· 如果你写的是「替换整个对象」,后一次会覆盖前一次(符合直觉)
· 不要写「依赖外部变量」的 updateFn(那会破坏「多次叠加」的正确性)
5.4 三条硬性约束
// ① ❌ 不能在事件处理里直接调用 addOptimistic(必须包在 Action / transition 里)
function Bad() {
const [optimistic, addOptimistic] = useOptimistic(list, reducer);
return ;
// ⚠️ 报错/警告:An optimistic state update occurred outside a transition or action
}
// ✅ 方式一:包在 form action 里(推荐)
// ② ⚠️ 乐观状态是「临时的」——不要拿它做「持久计算」
const [optimistic, addOptimistic] = useOptimistic(list, reducer);
const total = optimistic.length; // ✅ 用于显示
useEffect(() => { saveTotal(total); }, [total]); // ❌ 不要把它同步出去(会反复触发)
localStorage.setItem('items', JSON.stringify(optimistic)); // ❌ 不要持久化乐观值
// ③ ⚠️ 临时项的 key 必须唯一且稳定(用 crypto.randomUUID() 生成临时 id)
const tempId = `temp-${crypto.randomUUID()}`; // ✅
key={Math.random()} // ❌ 每次渲染都变 → 整列重建
| 维度 | useOptimistic(React 19) |
TanStack Query onMutate(见 7.6) |
|---|---|---|
| 回滚 | 自动(Action 结束即丢弃) | 手动(保存 previous + onError 回滚) |
| 控制力 | 低(不能控制回滚时机) | 高(可精细控制缓存与失效) |
| 与谁配合 | Actions(表单/transition) | Query 缓存 |
| 适用 | 表单类提交(评论、点赞、勾选) | 任何需要「操作缓存」的场景 |
| 是否需要数据层 | 否 | 是(TanStack Query) |
六、把五个 Hook 串起来:一个完整的评论区
// ============ app/comments/actions.ts(服务端) ============
'use server';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
import { getCurrentUser } from '@/lib/auth';
export async function addComment(formData: FormData) {
// ⭐⭐ Server Action 是公开端点:必须鉴权 + 校验
const user = await getCurrentUser();
if (!user) throw new Error('请先登录');
const text = String(formData.get('text') ?? '').trim();
const postId = String(formData.get('postId') ?? '');
if (!text || text.length > 500) throw new Error('内容不合法');
await db.comment.create({ data: { text, postId, userId: user.id } });
revalidatePath(`/posts/${postId}`); // ⭐ 让页面缓存失效
}
// ============ app/comments/comment-section.tsx(客户端) ============
'use client';
import { useActionState, useOptimistic, useRef } from 'react';
import { useFormStatus } from 'react-dom';
import { addComment } from './actions';
interface Comment { id: string; text: string; author: string; pending?: boolean }
function SubmitButton() {
const { pending } = useFormStatus(); // ⭐ 子组件读提交态
return ;
}
export function CommentSection({ postId, comments }: { postId: string; comments: Comment[] }) {
const formRef = useRef(null);
// ① 乐观更新:把「正在提交的评论」立刻显示出来
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(current: Comment[], text: string) => [
...current,
{ id: `temp-${crypto.randomUUID()}`, text, author: '我', pending: true },
]
);
// ② 结果 + pending:把错误信息展示给用户
const [state, formAction] = useActionState(
async (_prev: { error?: string }, formData: FormData) => {
const text = String(formData.get('text') ?? '').trim();
if (!text) return { error: '请输入内容' };
addOptimisticComment(text); // ⭐ 必须在 Action 内
formRef.current?.reset();
try {
await addComment(formData);
return {}; // 成功:清空错误
} catch (err) {
return { error: err instanceof Error ? err.message : '发送失败' };
}
},
{}
);
return (
{optimisticComments.map((c) => (
-
{c.author}:{c.text}
{c.pending && ···}
))}
);
}
⭐ 这个例子里,四个 Hook 各自负责什么:
七、Server Actions:与这些 Hook 的配合
// ① 定义(服务端文件)
'use server';
export async function updateProfile(formData: FormData) { /* ... */ }
// ② 三种调用方式
// 方式一:表单提交(最常见)
⭐⭐ Server Actions 的四条安全纪律(面试高频)
① 【必须鉴权】
它是「公开的 HTTP 端点」——任何人可以用 curl 直接调它。
❌ 相信客户端传来的 userId / role
✅ 在函数内部从 cookie/session 读当前用户(见 12.7)
② 【必须校验输入】
不能假设「只有我的表单会调它」——
FormData 里的任何字段都可能被伪造(含 hidden input、name、value)
③ 【不要返回敏感数据】
Action 的返回值会「序列化传回客户端」→ 别把 token、内部 ID、堆栈返出去
④ 【注意幂等】
Action 可能被「重复提交」(双击、网络重试)→ 写操作要考虑幂等键
⭐ 额外的工程实践:
· 把 Action 放在独立文件('use server' 文件级指令)
· Action 内做「参数 schema 校验」(zod)
· 用 revalidatePath / revalidateTag 控制缓存失效粒度
· 长耗时任务不要在 Action 里做(用户会一直 pending)→ 用「任务队列 + 轮询」
八、迁移与兼容
// ⚠️ useFormState → useActionState(改名)
// React 19 之前:
import { useFormState } from 'react-dom';
const [state, formAction] = useFormState(action, initialState);
// React 19:
import { useActionState } from 'react';
const [state, formAction, isPending] = useActionState(action, initialState);
// ⭐ 新版还多返回一个 isPending(不用再靠 useFormStatus 拿)
| 旧写法 | 新写法 | 说明 |
|---|---|---|
useFormState(react-dom) |
useActionState(react) |
改名 + 换模块 |
useFormStatus 拿 pending |
useActionState 的 isPending |
本组件内的提交用后者更直接 |
手写 isSubmitting state |
上面的两种 | 不再需要手写 |
forwardRef |
ref 作为 prop |
React 19 起可直接接收 |
defaultProps(函数组件) |
默认参数 | React 19 已移除 |
⭐ 什么时候用 useFormStatus、什么时候用 useActionState 的 isPending?
· useActionState 的 isPending:适合「**提交按钮就在表单所在的组件里**」
→ 直接用它,少抽一层组件
· useFormStatus:适合「**提交按钮是独立复用的组件**」(如 )
→ 它不需要把 pending 从父组件透传下去,自己就能读到
· ⭐ 两者可以同时用(各取所需)
九、这一组 API 特有的事故现场
// ① 在渲染中创建 Promise 给 use() → 无限挂起
const data = use(fetchData()); // ❌ 每次都新建 → 永远 pending
const [promise] = useState(() => fetchData()); // ✅
// ② 把 use() 包进 try/catch → Suspense 失效
try { const d = use(p); } catch {} // ❌
// ③ 用 useState 存「乐观值」→ 失去自动回滚
const [list, setList] = useState(comments); // ❌ 失败后不会自动恢复
setList((l) => [...l, temp]); // 还得自己写回滚
// ④ 在事件处理里调 addOptimistic(不在 Action/transition 里)
⭐ ⑮ 的深入解释:
`
十、面试延伸
- 「
use和useContext有什么区别?」
① 调用位置——use(Ctx)可以在条件与循环里调用(useContext必须遵守 Hook 的顶层规则);② 能读 Promise——use还可以读 Promise(未就绪时挂起到最近的 Suspense);③ 订阅粒度不变——两者都是「value 一变,所有消费者重渲染」(use没有让订阅变细);④useContext未废弃。注意:use仍必须在渲染期间调用,且不能放在try/catch里。 -
「为什么
use不能放在try/catch里?」
因为use的挂起机制就是「抛出一个 Promise」——React 在渲染时捕获这个 Promise 并去最近的Suspense渲染 fallback。如果外面包了try/catch,Promise 会被你自己捕获,React 就看不到它 → 不会挂起 → 你会拿到「一个 Promise 对象」而不是数据(渲染出[object Promise]或直接报错)。那错误怎么办?use只负责「等待」,Promise reject 时的错误会被抛出并由 ErrorBoundary 接管——所以标准组合仍是「ErrorBoundary 在外、Suspense 在内」。 -
「
use(promise)里的 Promise 为什么要『稳定』?」
因为它决定了「挂起多久」。如果在渲染中创建(use(fetchData())),那么每次渲染都会新建一个未完成的 Promise → React 每次都挂起 → 无限挂起(页面永远停在 fallback)。正确做法有三种:① 由上层组件创建后作为 prop 传下来;② 用useState惰性初始化(const [p] = useState(() => fetch()));③ 由缓存或服务端提供(React 官方推荐的是「Server Component 里启动请求、把 Promise 传给客户端组件」)。 -
「
useActionState解决了什么问题?」
它把「Action 的返回值」与「pending 状态」交给 React 管理,从而省掉三块手写代码:①e.preventDefault()+new FormData(e.currentTarget)——现在 Action 直接收到FormData;② 手写的isSubmitting——现在用isPending;③ 结果与错误状态——现在由state统一承载(且能拿到「上一次的返回值」)。它还额外提供了两点:Action 结束后 React 会自动重置非受控表单;第三个参数permalink支持「JS 未加载时的渐进增强提交」。 -
「
useFormStatus为什么必须在<form>的子组件里调用?」
因为 React 是通过「表单状态 Context」把pending等状态传下去的,而这个 Provider 由<form>元素内部提供。所以「和<form>同级的组件」处在 Provider 之外,读不到状态(拿到的是外层表单或初始值)。实践结论:需要pending的按钮/字段要抽成「form 的子组件」(这也让<SubmitButton>成为一个可复用组件,贴到哪个表单都能用);如果不想抽组件,可以直接用useActionState返回的isPending。 -
「
useOptimistic的回滚是怎么实现的?」
它没有回滚代码——因为乐观值本身就是「临时的叠加」:useOptimistic(realState, updateFn)返回的是「真实状态 + 尚未落地的乐观修改」。当你调用addOptimistic(v)时,UI 立刻变成叠加后的结果;一旦 Action 结束(无论成功还是失败),这个叠加就被丢弃,UI 自动回到「真实状态的渲染结果」。所以「失败自动回滚」是它的语义副作用,而不是「你写的回滚逻辑」。代价是你不能控制回滚时机(Action 一结束就回滚)。 -
「
useOptimistic和 TanStack Query 的onMutate有什么区别?」
① 回滚——useOptimistic自动(Action 结束即丢弃叠加),onMutate需要手动保存previous并在onError里回滚;② 控制力——onMutate更强(能精细控制缓存、失效、onSettled时机),useOptimistic更简单但不能控制回滚时机;③ 依赖——useOptimistic只需要「Actions」,onMutate需要「TanStack Query 的缓存」;④ 适用——useOptimistic适合表单类提交(评论、点赞、勾选),onMutate适合任何需要维护服务端缓存的场景。 -
「Server Action 有什么安全风险?」
它是「公开的 HTTP 端点」——任何人都可以构造请求直接调用它(不经过你的 UI)。所以必须守四条纪律:① 鉴权——在函数内部从 cookie/session 读当前用户,绝不能相信客户端传来的userId/role;② 输入校验——FormData里的每个字段都可能被伪造(包括 hidden input);③ 不返回敏感数据——返回值会序列化传回客户端;④ 幂等——可能被重复提交(双击、网络重试),写操作要考虑幂等键。 -
「
useOptimistic里的updateFn为什么不能依赖外部变量?」
因为多次调用addOptimistic时,React 会把这串更新「依次应用」(语义类似useReducer的队列)。updateFn(current, optimisticValue)里的current是「上一次应用后的状态」(含之前的乐观叠加),所以:① 「追加」类操作连续调用会正确累加;② 如果updateFn依赖了「外部变量」(如组件里的list快照),就会出现「用旧快照计算」的错误(与setState的闭包问题同源)。结论:updateFn应该是「只依赖current与optimisticValue的纯函数」。 -
「React 19 之后,表单还能用受控组件吗?」
能。这组 Actions API 是「新增一种更省代码的写法」,不是「废弃受控组件」。选择标准依然是「是否需要实时响应输入」(见 12.6):① 需要(实时校验、格式化、联动、字数统计)→ 受控(value+onChange);② 不需要(提交时取最终值)→ 用 Actions(<form action>+useActionState)。实践中常常混用:输入框受控(做实时校验),提交走 Action(useActionState管错误与 pending)。 -
「
useActionState和useFormStatus该用哪个拿 pending?」
看「提交按钮与表单是否在同一个组件里」:① 同一组件 → 用useActionState返回的isPending(少抽一层);② 按钮是独立复用的组件(<SubmitButton>)→ 用useFormStatus(它自己就能读到所属表单的状态,不需要父组件透传pending)。两者可以同时使用,各取所需。 -
「用 Actions 之后,乐观更新还需要自己写吗?」
大多数「表单类乐观更新」不需要——useOptimistic覆盖了「新增/删除/修改/点赞」这几类常见形态,且回滚免费。但两类场景仍需自己写:① 需要精细控制缓存(TanStack Query 的onMutate+invalidateQueries);② 乐观状态要「跨越多个 Action」存活(useOptimistic的值只活在单个 Action 期间,Action 一结束就回滚)。
一句话速记
React 19 这组 API 围绕「Action」展开:
use(渲染期读 Promise/Context,可在条件/循环里用,但不能放try/catch,Promise 必须稳定) +useActionState(统一管state与isPending,省掉preventDefault/FormData/手写 loading) +useFormStatus(子组件读pending/data/method,必须在<form>内) +useOptimistic(乐观值是「临时叠加」,Action 结束自动丢弃 = 自动回滚,updateFn必须是纯函数,addOptimistic必须在 Action/transition 内调用) + Server Actions('use server',是公开 HTTP 端点 → 必须鉴权 + 校验输入 + 不返回敏感数据 + 考虑幂等);useFormState已改名为useActionState;「高风险操作不要乐观更新」「大文件上传不要放 Action 里」;受控组件没有被废弃——「需要实时响应就用受控,只取提交值就用 Actions」,实践中常常混用。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!