Skip to content
This page has been auto-translated and may contain errors.View in English

表单

在普通 HTML 输入框中输入,浏览器会自己管理这个值:它存储你输入的内容,在没有任何代码帮助的情况下重新绘制光标。将这个输入框的 value 连接到一个 state 片段,并在 onChange 时更新这个 state,输入框就开始像 React 中的其他东西一样工作:state 保存着页面上内容的真实情况,输入框则反映这个状态。这就是 受控输入,React 在每一刻决定显示什么。

这是一个包含单个文本输入的简单表单:

jsx
function NameForm() {
  const [text, setText] = useState('')

  function handleSubmit(event) {
    event.preventDefault()
    console.log(text)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button>Submit</button>
    </form>
  )
}

输入框的 value 来自 text state,每次按键都会触发 onChange,调用 setText 更新新的值。state 和输入框始终保持同步,因为 React 每次都用 text 的最新值重新渲染输入框。提交表单默认会重新加载页面,所以 handleSubmit 先调用 event.preventDefault(),然后执行应用需要的操作,这里是打印文本,其他地方可能是发送到服务器。

受控和非受控输入

上面的输入框是受控输入,因为它的值存在 React state 中,React 决定显示什么。给它一个 value 但没有 onChange,输入框就变成只读的:React 在每次渲染时都会把同样的值画回去,你输入的任何内容都到不了 state,所以受控字段时要把这两个配合起来。输入框也可以是非受控的:DOM 自己跟踪它的值,你只在需要的时候读取,对单个字段可以通过 ref 读取(一种直接访问 DOM 元素的方式,详见 Refs and the DOM),对整个表单可以在提交时通过表单的 FormData 读取。受控输入在每次按键时都给组件提供当前值,这是实时验证、字符计数或 UI 其他部分依赖的字段所需要的。非受控输入跳过每次按键的重新渲染,只在提交时获取值,这样对于不需要其他部分监视的简单字段来说代码更少。

复选框

复选框遵循相同的模式,但它们的 state 存在 checked 而不是 value

jsx
function Newsletter() {
  const [subscribed, setSubscribed] = useState(false)

  return (
    <label>
      <input
        type="checkbox"
        checked={subscribed}
        onChange={e => setSubscribed(e.target.checked)}
      />
      Subscribe to updates
    </label>
  )
}

e.target.checked 是一个布尔值,subscribed 控制复选框是否被勾选:一个 prop,一个改变处理程序,和文本输入的结构相同。

选择框

<select> 也需要 value,但这个 value 设置在 select 自身而不是被选中的 option 上:

jsx
function ColorPicker() {
  const [color, setColor] = useState('red')

  return (
    <select value={color} onChange={e => setColor(e.target.value)}>
      <option value="red">Red</option>
      <option value="green">Green</option>
      <option value="blue">Blue</option>
    </select>
  )
}

select 上设置 value,React 会为你选中匹配的 option不需要给任何 option 元素添加 selected 属性。

文本域

在 HTML 中,textarea 的文本位于开标签和闭标签之间。React 把它当作像其他输入框一样的输入,所以文本通过 value prop 传入,onChange 保持它的同步:

jsx
function Feedback() {
  const [message, setMessage] = useState('')

  return (
    <textarea
      value={message}
      onChange={e => setMessage(e.target.value)}
      rows={4}
    />
  )
}

rows 设置文本框的初始高度。defaultValue 是这里 value 的非受控等价物:它设置初始文本一次,然后让 DOM 从那里跟踪它。

单选按钮

多个输入框共享一个 name,告诉浏览器它们属于同一组。只能选中其中一个。在 React 中这个组共享一个 state,每个输入框的 checked 与它进行比较:

jsx
function ShippingSpeed() {
  const [speed, setSpeed] = useState('standard')

  return (
    <fieldset>
      <legend>Shipping speed</legend>
      {['standard', 'express', 'overnight'].map(option => (
        <label key={option}>
          <input
            type="radio"
            name="speed"
            value={option}
            checked={speed === option}
            onChange={e => setSpeed(e.target.value)}
          />
          {option}
        </label>
      ))}
    </fieldset>
  )
}

fieldsetlegend 为屏幕阅读器标记这个组,每个 label 需要一个 key,因为选项来自数组(lists and keys)。

checked={speed === option} 对恰好一个选项为真,所以一个 state 值覆盖整个组,而复选框则各自携带自己的布尔值。

用 action 提交

<form> 也可以接受一个 action 函数。React 在表单提交时调用它,并传入一个 FormData 对象,其中包含表单的值:

jsx
function Signup() {
  function handleSignup(formData) {
    const values = Object.fromEntries(formData)
    console.log(values.email, values.password)
  }

  return (
    <form action={handleSignup}>
      <input name="email" type="email" />
      <input name="password" type="password" />
      <button>Sign up</button>
    </form>
  )
}

这里没有事件对象,所以没有什么要调用 preventDefault() 的。React 为你阻止浏览器的默认页面重新加载。

Object.fromEntries(formData) 一次性把 FormData 转换成普通对象。键来自每个输入框的 name 属性,这就是为什么表单中的每个输入框都需要一个 name。当多个输入框共享一个名称并可以一起发送时,比如复选框组,只有最后一个值会保留,使用 formData.getAll 可以读取它们全部。

上面的输入框是非受控的:DOM 保存每个值直到提交。对于从 onSubmit 来的人会感到惊讶的一个行为,这很重要:一旦 action 完成,React 会重置表单,清空这些非受控字段。受控字段从 state 重新渲染,保留它的值。

每条路径适合不同类型的表单。action 适合于那些工作是收集值并交出去的表单,通常交给服务器,等待是真实的,结果必须返回到 UI 中。值到达时是已收集和已命名的。带受控输入的 onSubmit 适合在用户输入时必须做出反应的表单,用于实时验证、字符计数或改变表单其余部分显示的字段。

版本说明

Form action 在 React 19 中推出。在 React 18 及更早版本上,<form action={handleSignup}> 不会抛出错误。React 会丢弃这个属性,因为函数不是一个有效的属性值,开发环境会警告你。表单随后会提交到当前 URL,页面重新加载,函数永远不会运行。这些版本上的提交通过 onSubmit 配合 event.preventDefault() 进行。

真实的表单很少只有一个字段。一个要求输入名字、邮箱和密码的注册表单可以跟踪三个单独的 useState 调用,但一个 state 对象和一个改变处理程序可以覆盖任意数量的字段而不用重复代码:

jsx
function SignupForm() {
  const [values, setValues] = useState({ name: '', email: '', password: '' })

  function handleChange(event) {
    const { name, value } = event.target
    setValues(prev => ({ ...prev, [name]: value }))
  }

  return (
    <form>
      <input name="name" value={values.name} onChange={handleChange} />
      <input name="email" value={values.email} onChange={handleChange} />
      <input type="password" name="password" value={values.password} onChange={handleChange} />
    </form>
  )
}

每个输入框的 name 属性匹配 values 中的一个键,所以 handleChange 读取 event.target.name 来知道要更新哪个键,[name]: value 把它写回同一个键下。给表单添加另一个字段,给它的输入框一个匹配的 name,现有的处理程序就已经覆盖了。

验证可以在两个不同的点运行,它们有不同的用途。在改变时验证,在 handleChange 中检查新值,会立即给出反馈,对密码强度计之类的东西很有用。这也意味着错误可能显示在用户还没完成输入的字段上。在提交时验证,在提交处理程序中检查整个 values 对象,等到用户完成了再说,这往往是必填字段和格式检查的更好选择。很多表单混合使用这两种:改变时进行轻微检查,提交时进行完整检查。

与服务器通信的提交需要时间,一个在等待期间仍然可点击的按钮会邀请第二次点击和重复提交。在 state 中跟踪一个 submitting 标志,在请求开始前设置它,当它为真时禁用按钮:

jsx
function SignupForm() {
  const [values, setValues] = useState({ name: '', email: '', password: '' })
  const [submitting, setSubmitting] = useState(false)

  async function handleSubmit(event) {
    event.preventDefault()
    setSubmitting(true)
    await saveSignup(values)
    setSubmitting(false)
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* inputs go here */}
      <button disabled={submitting}>{submitting ? 'Submitting...' : 'Sign up'}</button>
    </form>
  )
}

disabled={submitting} 在请求进行中时让按钮变灰并阻止点击,一旦请求完成就把控制权交回来。只是收集值并发送的表单可以把整个任务交给 action,那里 useActionState 包装 action 并给你那个待处理标志,而不需要自己连接。

受控输入中的每次按键都会进行一次往返:DOM 触发事件,你的处理程序调用 setText,React 重新渲染组件,新的 value prop 回到同一个输入框上。它感觉很快,但输入框显示的值在每次渲染时都被 React 设置。DOM 元素在这里没有自己的记忆,它显示 state 现在说的。

那个往返也是代价。一个有很多受控字段的表单在任何字段的每次按键时都重新渲染整个组件。通常那足够便宜,可以忽略。当不是这样时,或当一个字段的值在提交前不需要影响任何其他东西时,一个带 ref 的非受控输入是更简单的选择:

jsx
function NameForm() {
  const inputRef = useRef(null)

  function handleSubmit(event) {
    event.preventDefault()
    console.log(inputRef.current.value)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} defaultValue="" />
      <button>Submit</button>
    </form>
  )
}

没有 state,没有每次按键的重新渲染。defaultValue 在这里做的工作和它在 textarea 上做的一样。当一个字段完全隔离时考虑这个:在用户输入时没有其他东西重新渲染。

useActionState 包装一个 action 并交回一个待处理标志和 action 返回的任何东西,所以提交标志和错误消息停止成为你自己连接的单独的 state 片段。Server Action 和建立在其上的框架集成走得更远,在服务器上运行 action 本身,超过这个手册的范围。

JunoState drives the input 要记住的模式很小:输入框的 value 来自 state,onChange 更新那个 state。一旦这对文本输入点击了,对选择框和文本域工作方式相同,对复选框和单选按钮,checked 携带 state。提交是自己的一步:表单的提交运行你写的一个函数,那个函数决定用这些值做什么。
JunoState drives the input 受控输入是默认的:value(或对复选框的 checked)与 state 绑定,onChange 更新它,onSubmit 配合 event.preventDefault() 处理提交。对多字段表单,一个 state 对象和由 name 键入的 handleChange 替代了一堆单独的 useState 调用,一个 submitting 标志在请求进行中时保持按钮有效。表单上的 action 函数是另一条路线:React 传给它一个 FormData 对象并处理事件,一旦服务器参与,useActionState 接管那个提交标志。
JunoState drives the input 受控输入是 React 在每次渲染时拥有 DOM 的值,这就是给你一个在任何时候读取、验证或从中导出的值的原因。知道什么时候那个往返没有赚到它的保留,一个非受控 ref 将足够,当表单的工作是收集值并交出时,到达表单 action,一旦服务器参与,useActionState 携带待处理的 state。

接下来:Lifting state up,两个组件需要共享同一个 state 片段。