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

表单

在普通的 HTML 输入框中输入文字,浏览器会自动处理这个值:它存储你输入的内容并重新绘制光标,完全不需要你的代码。如果你把这个输入框的 value 连接到一个 state 上,并在 onChange 时更新这个状态,输入框就开始按照 React 的方式工作:状态掌握页面上显示什么的真实情况,输入框则反映这个状态。这就是受控输入,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 状态,每次按键都会触发 onChange,随后调用 setText 并传入新值。状态和输入框保持同步是因为 React 每次都根据 text 的当前值重新渲染输入框。提交表单默认会重新加载页面,所以 handleSubmit 先调用 event.preventDefault() 来阻止这个行为,然后执行应用需要做的事情,这里就是记录这个文本,或者在其他地方将它发送给服务器。

受控输入和非受控输入

上面的输入框是受控输入,因为它的值存在 React 状态中,React 决定了它显示什么。如果你给它一个 value 但没有 onChange,这个输入框就变成只读的:React 每次渲染都会把同样的值设置回去,你输入的任何东西都不会到达状态,所以当一个字段被控制时,一定要把这两个一起用。输入框也可以是非受控的:DOM 自己跟踪它的值,你只在需要的时候读取它,可以通过 ref 来读取单个字段(一种直接访问 DOM 元素的方法,在 Refs 和 DOM 中有详细说明),或者在提交时通过表单的 FormData 读取整个表单。受控输入在每次按键时都给组件当前的值,这对实时验证、字符计数,或者 UI 其他部分依赖的字段来说是必需的。非受控输入则跳过了每次按键带来的一次重新渲染,只在提交时读取值,对于不需要其他地方关注的简单字段来说代码更少。

复选框

复选框遵循相同的模式,但它们的状态存在 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 中这个组共享一个状态,每个输入框的 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 因为选项来自一个数组(列表和键)。

checked={speed === option} 对于恰好一个选项是真,所以一个状态值覆盖了整个组,而复选框则各自携带一个布尔值。

使用 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 会重置表单,清除这样的非受控字段。受控字段从状态重新渲染并保持它的值。

这两条路适合不同类型的表单。一个 action 适合其工作是收集值并交付它们的表单,通常是交给服务器,那里的等待是真实的,结果必须回到 UI。值来时已经被收集和命名了。带有受控输入的 onSubmit 适合必须在用户输入时做出反应的表单,用于实时验证、字符计数,或改变表单其余部分所显示内容的字段。

版本说明

Form actions 在 React 19 中出现。在 React 18 及更早版本中,<form action={handleSignup}> 不会抛出错误。React 会删除这个属性,因为函数不是一个有效的属性值,并在开发时会对此发出警告。表单随后会提交到当前 URL,页面会重新加载,函数永远不会运行。在那些版本中的提交需要通过带有 event.preventDefault()onSubmit

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

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 对象,会等到用户完成,这通常对于必填字段和格式检查是更好的默认行为。很多表单混合这两种:在改变时进行轻量级检查,在提交时进行完整检查。

与服务器通话的提交需要时间,一个在这段等待期间保持可点击的按钮会引发第二次点击和重复提交。在状态中跟踪一个 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 元素这里没有自己的记忆,它显示的就是状态此刻说的。

那个往返也是成本。一个有许多受控字段的表单会在任何字段中的每一次按键时重新渲染整个组件。通常这便宜到足以忽略。当它不是时,或当一个字段的值在提交前都不需要影响任何其他东西时,一个带 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>
  )
}

没有状态,没有每次按键时的重新渲染。defaultValue 在这里做的工作和它在 textarea 上做的一样。当一个字段是完全隔离的时到达这个:当用户在其中输入时没有其他东西会以不同的方式渲染。

useActionState 包装一个 action 并交回一个待定标志以及 action 返回的任何东西,所以一个提交标志和一个错误消息不再是你自己接管的分开的状态部分。Server Actions 和建立在它们之上的框架集成会进一步推进这个,在服务器上运行 action 本身,这超过了这个手册的范围。

JunoState drives the input 要记住的模式很小:输入框的 value 来自状态,onChange 更新那个状态。一旦这对文本输入框点击了,它对选择框和文本区域的工作方式相同,对于复选框和单选按钮的工作方式也相同,只是用 checked 来携带状态。

提交是它自己的一个步骤:表单的提交运行你写的一个函数,那个函数决定了用这些值发生什么。

JunoState drives the input 受控输入是默认的:value(或对于复选框的 checked)连接到状态,onChange 更新它,onSubmit 带有 event.preventDefault() 处理提交。

对于多字段表单,一个状态对象和一个由 name 索引的 handleChange 替代了一堆分开的 useState 调用,一个 submitting 标志在请求进行中时让按钮诚实。

表单上的一个 action 函数是另一条路:React 把一个 FormData 对象交给它并处理事件,一旦涉及服务器,useActionState 接管那个提交标志。

JunoState drives the input 受控输入是 React 在每次渲染时拥有 DOM 的值,这就是在任何点给你一个值来读取、验证或从中派生的东西。

知道那个往返何时不值得,一个非受控 ref 就足够了,当表单的工作是收集值并交付它们时到达表单 action,一旦涉及服务器就用 useActionState 携带待定状态。

接下来:提升状态,两个组件需要共享同一个状态。