增强的表单操作

在 React 19 之前的版本中,表单的操作能够讲述的内容主要是受控组件和非受控组件。对于表单整体的操作基本上 React 是不怎么涉及的。但是在 React 19 中由于强化了 Server Component,对于表单的操作支持也开始变得有必要起来。

在不使用其他的支持库处理表单的时候,传统的表单操作大致都是这样的。

const LoginForm = () => {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [isPending, setPending] = useState(false);
  const [error, setError] = useState(null);

  const handleSubmit = useCallback(
    async (e: React.FormEvent<HTMLFormElement>) => {
      e.preventDefault();
      setError(null);
      setPending(true);

      try {
        await login(username, password);
      } catch (error) {
        setError(error);
      } finally {
        setPending(false);
      }
    },
    [username, password]
  );

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="usename">Username</label>
        <input
          type="text"
          id="username"
          name="username"
          onChange={(e) => setUsername(e.target.value)}
        />
      </div>
      <div>
        <label htmlFor="password">Password</label>
        <input
          type="password"
          id="password"
          name="password"
          onChange={(e) => setPassword(e.target.value)}
        />
      </div>
      {error && <div>{error}</div>}
      <button type="submit" disabled={isPending}>
        {isPending ? "Processing..." : "Login"}
      </button>
    </form>
  );
};

在这个示例里,整个表单的处理看上去就十分的繁琐,但是其中每一个步骤却还都是十分必要的。但是在 React 19 引入了一个新的 Hook useActionState以后,上面这个示例就可以被大大的简化了。

先来看示例。

const LoginForm = () => {
  const [(state, formAction, isPending)] = useActionState(
    async (prevState, formData) => {
      try {
        await login(formData.get("username"), formData.get("password"));
        return { error: null };
      } catch (error) {
        return { error };
      }
    },
    { error: null }
  );

  return (
    <form action={handleSubmit}>
      <div>
        <label htmlFor="usename">Username</label>
        <input type="text" id="username" name="username" />
      </div>
      <div>
        <label htmlFor="password">Password</label>
        <input type="password" id="password" name="password" />
      </div>
      {state.error && <div>{state.error}</div>}
      <button type="submit" disabled={isPending}>
        {isPending ? "Processing..." : "Login"}
      </button>
    </form>
  );
};

可以看到在换用useActionState以后,表单处理的整体代码两变少了,而且结构也更加清晰了。

useActionState的语法格式为const [state, formAction, isPending] = useActionState(fn, initialState, permalLink?);。在useActionState返回的state中,保存的是参数fn执行返回的结果,如果表单还没有被提交,那么state的内容就将是initialState的内容。formActionuseActionState生成的用于提供给<form>action属性或者是<button>formAction属性使用的用于处理表单提交的函数。isPending则是用来表示当前formAction的执行状态的。

Tip

除了<button>可以使用formAction属性以外,<input type="submit"><input type="image">也是可以使用formAction属性的。

useActionState中的使用到的fn参数实际上就是在表单提交的时候实际调用的函数。这个函数参数要求的类型是(prevState: any, formData: FormData) => any。从这个函数参数的类型可以看出来,表单在提交的时候会首先将当前的最新状态作为第一个参数传入,然后将传统表单数据作为第二个参数传入,这个函数执行结束的返回值将作为useActionState的新状态。

这样结合上面的示例就可以彻底明白useActionState中的各个元素都是如何使用的了。

Tip

useActionState的第三个参数permalink是在服务器渲染处理中用来结合渐进增强处理使用的,如果表单在Javascript包夹在之前提交,那么浏览器将会导航到permalink指定的URL。在表单被激活以后,permalink将会失效。

<form>actiononSubmit的区别

action是 React DOM 19 中新引入的属性,主要用来支持表单的提交操作。与onSubmit最大的一个不同是onSubmit仅支持在客户端处理表单,而action则支持在客户端和服务端均支持表单的提交处理。

此外就是action会传递当前表单的状态和表单提交数据给处理函数,而onSubmit传递的则是表单提交事件。

Tip

在不使用服务端渲染的时候,action所执行的动作与onSubmit一致。

useFormStatus

useFormStatus这个 Hook 是 React DOM 19 中提供的,用来获取当前表单的提交状态的,它的调用不接受任何参数,但是会返回一系列表单相关的属性,其常用格式为const {pending, data, method, action} = useFormStatus()

这个 Hook 对于实现一个独立的提交功能很方便。例如可以如同下面这个示例中一样实现一个独立的提交按钮。

const Submit = () => {
  const { pending } = useFormStatus();

  return <button disabled={pending}>Submit</button>;
};

function App() {
  const [state, action] = useActionState(
    async (prev, formData) => {
      // 省略表单数据的处理
    },
    {
      error: null,
    }
  );

  return (
    <form action={action}>
      <Submit />
    </form>
  );
}

Caution

useFormStatus需要在<form>元素的子级组件中使用,使用在<form>组件的同级组件中是不会有任何效果的。