表单
在普通的 HTML 输入框中输入文字,浏览器会自动处理这个值:它存储你输入的内容并重新绘制光标,完全不需要你的代码。如果你把这个输入框的 value 连接到一个 state 上,并在 onChange 时更新这个状态,输入框就开始按照 React 的方式工作:状态掌握页面上显示什么的真实情况,输入框则反映这个状态。这就是受控输入,React 在每一刻都决定了什么会显示出来。
这是一个包含一个文本输入框的简单表单:
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 中:
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 上:
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 让它保持同步:
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 都和它进行比较:
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>
)
}fieldset 和 legend 为屏幕阅读器标记这个组,每个 label 有一个 key 因为选项来自一个数组(列表和键)。
checked={speed === option} 对于恰好一个选项是真,所以一个状态值覆盖了整个组,而复选框则各自携带一个布尔值。
使用 action 提交
<form> 也可以接受一个 action 函数。当表单被提交时 React 会调用它,并传给它一个包含表单值的 FormData 对象:
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。
value 来自状态,onChange 更新那个状态。一旦这对文本输入框点击了,它对选择框和文本区域的工作方式相同,对于复选框和单选按钮的工作方式也相同,只是用 checked 来携带状态。 提交是它自己的一个步骤:表单的提交运行你写的一个函数,那个函数决定了用这些值发生什么。
接下来:提升状态,两个组件需要共享同一个状态。

