React 表单
表单不同于其他 HTML 元素,因为它要响应用户的交互,显示不同的状态,所以在 React 里面会有点特殊。
在 React 里,HTML 表单元素的工作方式和其他的 DOM 元素有些不同,这是因为表单元素通常会保持一些内部的 state。例如这个纯 HTML 表单只接受一个名称:
<form> <label> 名字: <input type="text" name="name" /> </label> <input type="submit" value="提交" /> </form>
此表单具有默认的 HTML 表单行为,即在用户提交表单后浏览到新页面。如果你在 React 中执行相同的代码,它依然有效。但大多数情况下,使用 JavaScript 函数可以很方便的处理表单的提交, 同时还可以访问用户填写的表单数据。实现这种效果的标准方式是使用“受控组件”。
状态属性
表单元素有这么几种属于状态的属性:
value,对应 <input> 和 <textarea> 所有
checked,对应类型为 checkbox 和 radio 的 <input> 所有
selected,对应 <option> 所有
在 HTML 中 <textarea> 的值可以由子节点(文本)赋值,但是在 React 中,要用 value 来设置。
表单元素包含以上任意一种状态属性都支持 onChange 事件监听状态值的更改。
针对这些状态属性不同的处理策略,表单元素在 React 里面有两种表现形式。
受控组件
在 HTML 中,表单元素(如<input>、 <textarea> 和 <select>)之类的表单元素通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。
我们可以把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。
例如,如果我们想让前一个示例在提交时打印出名称,我们可以将表单写为受控组件:
class NameForm extends React.Component { constructor(props) { super(props); this.state = {value: ''}; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } handleChange(event) { this.setState({value: event.target.value}); } handleSubmit(event) { alert('提交的名字: ' + this.state.value); event.preventDefault(); } render() { return ( <form onSubmit={this.handleSubmit}> <label> 名字: <input type="text" value={this.state.value} onChange={this.handleChange} /> </label> <input type="submit" value="提交" /> </form> ); } }
由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源。由于 handlechange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。
对于受控组件来说,每个 state 突变都有一个相关的处理函数。这使得修改或验证用户输入变得简单。例如,如果我们要强制要求所有名称都用大写字母书写,我们可以将 handlechange 改写为:
handleChange(event) { this.setState({value: event.target.value.toUpperCase()}); }
对于设置了上面提到的对应“状态属性“值的表单元素就是受控表单组件,比如:
render: function() { return <input type="text" value="hello"/>; }
一个受控的表单组件,它所有状态属性更改涉及 UI 的变更都由 React 来控制(状态属性绑定 UI)。比如上面代码里的 <input> 输入框,用户输入内容,用户输入的内容不会显示(输入框总是显示状态属性 value的值 hello),这有点颠覆我们的认知了,所以说这是受控组件,不是原来默认的表单元素了。
如果你希望输入的内容反馈到输入框,就要用 onChange 事件改变状态属性 value 的值:
getInitialState: function() { return {value: 'hello'}; }, handleChange: function(event) { this.setState({value: event.target.value}); }, render: function() { var value = this.state.value; return <input type="text" value={value} onChange={this.handleChange} />; }
使用这种模式非常容易实现类似对用户输入的验证,或者对用户交互做额外的处理,比如截断最多输入140个字符:
handleChange: function(event) { this.setState({value: event.target.value.substr(0, 140)}); }
非受控组件
和受控组件相对,如果表单元素没有设置自己的“状态属性”,或者属性值设置为 null,这时候就是非受控组件。
它的表现就符合普通的表单元素,正常响应用户的操作。
同样,你也可以绑定 onChange 事件处理交互。
如果你想要给“状态属性”设置默认值,就要用 React 提供的特殊属性 defaultValue,对于 checked 会有 defaultChecked,<option> 也是使用 defaultValue。
为什么要有受控组件?
引入受控组件不是说它有什么好处,而是因为 React 的 UI 渲染机制,对于表单元素不得不引入这一特殊的处理方式。
在浏览器 DOM 里面是有区分 attribute 和 property 的。attribute 是在 HTML 里指定的属性,而每个 HTML 元素在 JS 对应是一个 DOM 节点对象,这个对象拥有的属性就是 property(可以在 console 里展开一个 DOM 节点对象看一下,HTML attributes 只是对应其中的一部分属性),attribute 对应的 property 会从 attribute 拿到初始值,有些会有相同的名称,但是有些名称会不一样,比如 attribute class 对应的 property 就是 className。(详细解释:.prop,.prop() vs .attr())
回到 React 里的 <input> 输入框,当用户输入内容的时候,输入框的 value property 会改变,但是 value attribute 依然会是 HTML 上指定的值(attribute 要用 setAttribute 去更改)。
React 组件必须呈现这个组件的状态视图,这个视图 HTML 是由 render 生成,所以对于
render: function() { return <input type="text" value="hello"/>; }
在任意时刻,这个视图总是返回一个显示 hello 的输入框。
<select>
在 HTML 中 <select> 标签指定选中项都是通过对应 <option> 的 selected 属性来做的,但是在 React 修改成统一使用 value。
所以没有一个 selected 的状态属性。
<select value="B"> <option value="A">Apple</option> <option value="B">Banana</option> <option value="C">Cranberry</option> </select>
你可以通过传递一个数组指定多个选中项:<select multiple={true} value={['B', 'C']}>
textarea 标签
在 HTML 中, <textarea> 元素通过其子元素定义其文本:
<textarea> 你好, 这是在textarea里的文本 </textarea>
而在 React 中,<textarea> 使用 value 属性代替。这样,可以使得使用 <textarea> 的表单和使用单行 input 的表单非常类似:
class EssayForm extends React.Component { constructor(props) { super(props); this.state = { value: '请撰写一篇关于你喜欢的 DOM 元素的文章.' }; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } handleChange(event) { this.setState({value: event.target.value}); } handleSubmit(event) { alert('提交的文章: ' + this.state.value); event.preventDefault(); } render() { return ( <form onSubmit={this.handleSubmit}> <label> 文章: <textarea value={this.state.value} onChange={this.handleChange} /> </label> <input type="submit" value="提交" /> </form> ); } }
请注意,this.state.value 初始化于构造函数中,因此文本区域默认有初值。
select 标签
在 HTML 中,<select> 创建下拉列表标签。例如,如下 HTML 创建了水果相关的下拉列表:
<select> <option value="grapefruit">葡萄柚</option> <option value="lime">酸橙</option> <option selected value="coconut">椰子</option> <option value="mango">芒果</option> </select>
请注意,由于 selected 属性的缘故,椰子选项默认被选中。React 并不会使用 selected 属性,而是在根 select 标签上使用 value 属性。这在受控组件中更便捷,因为您只需要在根标签中更新它。例如:
class FlavorForm extends React.Component { constructor(props) { super(props); this.state = {value: 'coconut'}; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } handleChange(event) { this.setState({value: event.target.value}); } handleSubmit(event) { alert('你喜欢的风味是: ' + this.state.value); event.preventDefault(); } render() { return ( <form onSubmit={this.handleSubmit}> <label> 选择你喜欢的风味: <select value={this.state.value} onChange={this.handleChange}> <option value="grapefruit">葡萄柚</option> <option value="lime">酸橙</option> <option value="coconut">椰子</option> <option value="mango">芒果</option> </select> </label> <input type="submit" value="提交" /> </form> ); } }
总的来说,这使得 <input type="text">, <textarea> 和 <select> 之类的标签都非常相似—它们都接受一个 value 属性,你可以使用它来实现受控组件。
注意
你可以将数组传递到 value 属性中,以支持在 select 标签中选择多个选项:
<select multiple={true} value={['B', 'C']}>
文件 input 标签
在 HTML 中,<input type=“file”> 允许用户从存储设备中选择一个或多个文件,将其上传到服务器,或通过使用 JavaScript 的 File API 进行控制。
<input type="file" />
因为它的 value 只读,所以它是 React 中的一个非受控组件。将与其他非受控组件在后续文档中一起讨论。
处理多个输入
当需要处理多个 input 元素时,我们可以给每个元素添加 name 属性,并让处理函数根据 event.target.name 的值选择要执行的操作。
例如:
class Reservation extends React.Component { constructor(props) { super(props); this.state = { isGoing: true, numberOfGuests: 2 }; this.handleInputChange = this.handleInputChange.bind(this); } handleInputChange(event) { const target = event.target; const value = target.type === 'checkbox' ? target.checked : target.value; const name = target.name; this.setState({ [name]: value }); } render() { return ( <form> <label> 参与: <input name="isGoing" type="checkbox" checked={this.state.isGoing} onChange={this.handleInputChange} /> </label> <br /> <label> 来宾人数: <input name="numberOfGuests" type="number" value={this.state.numberOfGuests} onChange={this.handleInputChange} /> </label> </form> ); } }
这里使用了 ES6 计算属性名称的语法更新给定输入名称对应的 state 值:
例如:
this.setState({ [name]: value });
等同 ES5:
var partialState = {}; partialState[name] = value; this.setState(partialState);
另外,由于 setState() 自动将部分 state 合并到当前 state, 只需调用它更改部分 state 即可。
受控输入空值
在受控组件上指定 value 的 prop 可以防止用户更改输入。如果指定了 value,但输入仍可编辑,则可能是意外地将value 设置为 undefined 或 null。
下面的代码演示了这一点。(输入最初被锁定,但在短时间延迟后变为可编辑。)
ReactDOM.render(<input value="hi" />, mountNode); setTimeout(function() { ReactDOM.render(<input value={null} />, mountNode); }, 1000);
受控组件的替代品
有时使用受控组件会很麻烦,因为你需要为数据变化的每种方式都编写事件处理函数,并通过一个 React 组件传递所有的输入 state。当你将之前的代码库转换为 React 或将 React 应用程序与非 React 库集成时,这可能会令人厌烦。在这些情况下,你可能希望使用非受控组件, 这是实现输入表单的另一种方式。
成熟的解决方案
如果你想寻找包含验证、追踪访问字段以及处理表单提交的完整解决方案,使用 Formik 是不错的选择。然而,它也是建立在受控组件和管理 state 的基础之上 —— 所以不要忽视学习它们。