Menu

React useActionState 教程:表单、错误与等待状态

useActionState 包装一个异步 action,保存它上一次返回的值,并告诉你它是否仍在运行。了解它的签名、表单 action、如何返回校验错误,以及如何在表单之外调用它。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

useActionState 是 React 19 的一个 Hook,它运行一个异步函数(即 action),把它返回的结果存为 state,并告诉你它是否仍在运行。调用方式是 const [state, formAction, isPending] = useActionState(action, initialState),然后把 formAction 传给一个 <form>。

先提交空表单:action 返回一个错误对象,提示信息随之出现。然后输入一个带 @ 的地址,再提交一次。按钮会在一秒钟内显示“Subscribing...”并处于禁用状态。

语法

const [state, formAction, isPending] = useActionState(action, initialState, permalink?);
  • action 是要运行的函数。React 以 action(previousState, ...args) 的形式调用它。它返回的值(如果是异步函数,则是它 resolve 的值)会成为新的 state。
  • initialState 是 action 从未运行过时的 state。让它和 action 返回值的结构保持一致,这样组件在第一次渲染时也能读取 state.message。
  • state 是最近一次的结果。
  • formAction 是包装过的 action。把它传给 <form action> 或 <button formAction>,或者自己调用。
  • isPending 从 action 开始运行的那一刻起为 true,直到它结束。
  • permalink 是可选的,只在框架中使用 Server Function 时才有意义:如果表单在 JavaScript 加载完成之前就被提交,它就是表单要跳转到的 URL。

action 的第一个参数是上一次的 state。这是迁移表单时最常见的错误:写成 async function save(formData),formData 里拿到的就是旧的 state,formData.get 会抛出错误。

用 FormData 读取表单

当 formAction 用作表单的 action 时,React 会以表单的 FormData 调用你的 action。每个带有 name 的字段都在其中。你不需要为输入框写 useState 或 onChange 处理函数,也不用调用 e.preventDefault():React 会处理提交事件。

取消勾选 Spicy 再提交一次:打印出的数据里少了 spicy 这个键,因为未勾选的复选框什么都不会发送。action 还读取了 previousState.count 来加一,这就是上一次的 state 排在第一位的原因。

返回校验错误

校验应该放在 action 里。把错误作为 state 的一部分返回,同时返回用户输入的值,这样表单就能把它们填回去。表单 action 完成后,React 会重置表单中的非受控字段,所以没有 defaultValue={state.values...} 的字段在提交失败后会变成空的。

输入只有一个字母的名字和一个很短的密码,然后提交。两条错误都会出现,两个字段也都保留了你输入的内容。删掉 defaultValue={state.values.name} 再提交一次:出错后名字字段会被清空。

在真实应用中,最终裁决要交给服务器。客户端校验能给出快速的反馈,但任何人都可以绕过你的表单直接发送请求。

在表单之外调用 action

formAction 是一个函数,所以按钮或 Effect 也可以调用它。自己调用时,要把调用包在 startTransition 中。表单会替你做这件事,普通的 onClick 则不会;没有 transition,isPending 就无法正确更新(React 会在开发环境中为此打印一条错误)。

点击几次 Load more。每次调用都从上一次返回的 state 开始,所以页面会按顺序追加。如果在 action 仍在运行时再次调用它,React 会把这次调用排进队列,等当前调用结束后再运行,每次都使用最新的 state。

useActionState 的由来

在 React 19 正式发布之前,这个 Hook 以 useFormState 的名字出现在 canary 构建中,从 react-dom 导入。后来它被重命名为 useActionState,移到了 react 中,并新增了 isPending 这个值。如果你遇到这样的旧代码,只需修改导入和名字:

// React canary, before 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);

想在嵌套很深的提交按钮里显示等待状态,可以用 react-dom 的 useFormStatus,它无需 props 就能读取父级表单的状态。React 19 概览把它和其他表单功能一起列了出来。

错误处理,以及何时该用别的方案

如果 action 抛出错误,React 会显示最近的错误边界,而不是更新 state。对于用户可以修正的错误,比如校验失败或用户名已被占用,就像注册示例那样把它们作为 state 返回。真正的故障再让它抛出。

useActionState 用于处理 action 的结果。如果某个值需要在 action 运行期间立即更新,比如点击后应该马上加一的点赞数,就把它和 useOptimistic 结合使用。对于每次按键都会变化、并在渲染期间读取的输入,普通的 useState 加受控输入框仍然是合适的工具。

常见错误

忘了上一次 state 这个参数。 async function save(formData) 会在 formData 中收到上一次的 state。用于表单时,签名永远是 (previousState, formData)。

输入框没有 name。 FormData 只包含带有 name 属性的字段。只有 id 的字段不会出现在 formData.get(...) 中,它会返回 null。

到处都用受控输入框。 在表单 action 中你仍然可以使用 value 和 onChange,但并非必须。带有 name 和 defaultValue 的非受控字段与 action 更契合,而且 React 在提交后的重置只影响非受控字段。

在 action 内部读取 state。 组件中的 state 是创建这个处理函数的那次渲染中的值。在 action 内部要使用 previousState,它永远是前一次调用的结果,即使有多次调用在排队也是如此。

为预期中的错误抛出异常。 抛出的错误会让最近的错误边界取代整个表单。密码错误并不是崩溃;把它作为 state 返回。

常见问题

useActionState 返回什么?

一个包含三项的数组:当前 state(action 上一次返回的值,action 运行之前则是初始 state),一个可以传给表单或由你自己调用的新 action,以及 isPending,action 运行期间它为 true。

action 会收到哪些参数?

第一个是上一次的 state,之后是调用 action 时传入的参数。在 <form action={formAction}> 中,第二个参数就是表单的 FormData。

useActionState 和 useFormState 有什么区别?

它们是同一个 Hook。它最初以 useFormState 的名字出现在 React 的 canary 版本中,属于 react-dom;到了 React 19,它被重命名为 useActionState 并移到 react 中,同时新增了 isPending 这个值。

可以在没有表单的情况下使用 useActionState 吗?

可以。你自己在 startTransition 中调用返回的 action 即可,比如在按钮的 onClick 里。如果没有 transition,isPending 无法正确更新,React 还会在开发环境中打印一条错误。

为什么提交后表单被清空了?

表单 action 完成后,React 会重置表单中的非受控字段,就像浏览器在普通提交后所做的那样。如果想在出错后保留某个值,就把它放在 state 中返回,再传给该字段的 defaultValue。

Coddy 编程语言插图

和 Coddy 一起学编程

开始