表单
在普通 HTML 输入框中输入,浏览器会自己管理这个值:它存储你输入的内容,在没有任何代码帮助的情况下重新绘制光标。将这个输入框的 value 连接到一个 state 片段,并在 onChange 时更新这个 state,输入框就开始像 React 中的其他东西一样工作:state 保存着页面上内容的真实情况,输入框则反映这个状态。这就是 受控输入,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 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:
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 中这个组共享一个 state,每个输入框的 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,因为选项来自数组(lists and keys)。
checked={speed === option} 对恰好一个选项为真,所以一个 state 值覆盖整个组,而复选框则各自携带自己的布尔值。
用 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 会重置表单,清空这些非受控字段。受控字段从 state 重新渲染,保留它的值。
每条路径适合不同类型的表单。action 适合于那些工作是收集值并交出去的表单,通常交给服务器,等待是真实的,结果必须返回到 UI 中。值到达时是已收集和已命名的。带受控输入的 onSubmit 适合在用户输入时必须做出反应的表单,用于实时验证、字符计数或改变表单其余部分显示的字段。
版本说明
Form action 在 React 19 中推出。在 React 18 及更早版本上,<form action={handleSignup}> 不会抛出错误。React 会丢弃这个属性,因为函数不是一个有效的属性值,开发环境会警告你。表单随后会提交到当前 URL,页面重新加载,函数永远不会运行。这些版本上的提交通过 onSubmit 配合 event.preventDefault() 进行。
value 来自 state,onChange 更新那个 state。一旦这对文本输入点击了,对选择框和文本域工作方式相同,对复选框和单选按钮,checked 携带 state。提交是自己的一步:表单的提交运行你写的一个函数,那个函数决定用这些值做什么。 接下来:Lifting state up,两个组件需要共享同一个 state 片段。

