陌上人如玉
公子世无双

2.15 React 19 新 Hooks 与 Actions

2.15 React 19 新 Hooks 与 Actions

一句话: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 的分工:

  谁提交?      → 
(或 startTransition 包一个异步函数) 提交中怎么显示?→ useFormStatus(子组件)/ useActionState 的 isPending(本组件) 提交结果放哪? → useActionState 的 state(返回值) 立刻反馈怎么办?→ useOptimistic(乐观更新,失败自动回滚) 服务端数据怎么读?→ use(promise) 或 Server Component 里直接 await ⚠️ 注意:这些 Hook 全部围绕「Action」这一个概念。 「Action」= 「一个可以是 async 的函数,由 React 负责调度与状态管理」。

二、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 ( {state.fieldErrors?.email && } {state.fieldErrors?.password &&

{state.fieldErrors.password}

} {state.error &&

{state.error}

} ); }
⭐ 它相比「手写 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 (
    

© 2017-2026   陌上寒   网站地图 京ICP备16036924号

请求次数:123 次,加载用时:1.227 秒,内存占用:74.90 MB

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫