前言
本文将详细介绍 React 中的表单处理方式,包括受控组件和非受控组件的概念、实现方法及其优缺点。通过本文,读者将能够更好地理解和使用 React 中的表单处理机制,提升开发效率和用户体验。
在 React 里,HTML 表单元素的工作方式和其他的 DOM 元素有些不同,这是因为表单元素通常会保持一些内部的 state。例如这个纯 HTML 表单只接受一个名称:
html
<form>
<label>
名字:
<input type="text" name="name" />
</label>
<input type="submit" value="提交" />
</form>
此表单具有默认的 HTML 表单行为,即在用户提交表单后浏览到新页面。如果你在 React 中执行相同的代码,它依然有效。但大多数情况下,使用 JavaScript 函数可以很方便的处理表单的提交, 同时还可以访问用户填写的表单数据。实现这种效果的标准方式是使用"受控组件"。
1、受控组件
在 HTML 中,表单元素(如、 和 )通常自己维护 state,并根据用户输入进行更新。
而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用setState()
来更新。
我们可以把两者结合起来,使 React 的 state 成为"唯一数据源"。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作-onChange。
被 React 以这种方式控制取值的表单输入元素就叫做"受控组件"。
例如,如果我们想让前一个示例在提交时打印出名称,我们可以将表单写为受控组件:
javascript
import React, { Component } from 'react'
class App extends Component {
state = {
username: ''
}
changeUsername = (e) => {
this.setState({
username: e.target.value
})
}
getData (e) {
e.preventDefault()
console.log(this.state.username)
}
render () {
return (
<form onSubmit = { this.getData.bind(this) }>
<div>
<input type="text" value={ this.state.username } onChange = { this.changeUsername }/>
</div>
<input type="submit" value="提交"/>
</form>
)
}
}
export default App
由于在表单元素上设置了 value
属性,因此显示的值将始终为 this.state.value
,这使得 React 的 state 成为唯一数据源。由于 handlechange
在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。
对于受控组件来说,输入的值始终由 React 的 state 驱动。你也可以将 value 传递给其他 UI 元素,或者通过其他事件处理函数重置,但这意味着你需要编写更多的代码。
2、textarea 标签
在 HTML 中, <textarea>
元素通过其子元素定义其文本:
<textarea>
你好, 这是在 text area 里的文本
</textarea>
而在 React 中,<textarea>
使用 value
属性代替。这样,可以使得使用 <textarea>
的表单和使用单行 input 的表单非常类似:
javascript
import React, { Component } from 'react'
class App extends Component {
state = {
username: ''
}
changeUsername = (e) => {
this.setState({
username: e.target.value
})
}
getData (e) {
e.preventDefault()
console.log(this.state.username)
}
render () {
return (
<form onSubmit = { this.getData.bind(this) }>
<div>
<textarea value={ this.state.username } onChange = { this.changeUsername }></textarea>
</div>
<input type="submit" value="提交"/>
</form>
)
}
}
export default App
请注意,this.state.value
初始化于构造函数中,因此文本区域默认有初值。
3、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
属性。这在受控组件中更便捷,因为您只需要在根标签中更新它。例如:
javascript
import React, { Component } from 'react'
class App extends Component {
state = {
val: ''
}
getData (e) {
e.preventDefault()
console.log(this.state.val)
}
render () {
return (
<form onSubmit = { this.getData.bind(this) }>
<div>
<select value = { this.state.val } onChange = { (e) => {
this.setState({
val: e.target.value
})
} }>
{/* 表达式的初始值未能匹配任何选项,
<select> 元素将被渲染为"未选中"状态。
在 iOS 中,这会使用户无法选择第一个选项。
因为这样的情况下,iOS 不会触发 change 事件。 */}
<option value="" disabled>请选择</option>
<option value="篮球">篮球</option>
<option value="皮球">皮球</option>
<option value="网球">网球</option>
<option value="足球">足球</option>
</select>
</div>
<input type="submit" value="提交"/>
</form>
)
}
}
export default App
总的来说,这使得, <input type="text">
, <textarea>
和 <select>
之类的标签都非常相似---它们都接受一个 value
属性,你可以使用它来实现受控组件。
注意
你可以将数组传递到
value
属性中,以支持在select
标签中选择多个选项:<select multiple={true} value={['B', 'C']}>
参考 --- 不讲解
javascript
class MulFlavorForm extends React.Component {
constructor(props) {
super(props);
this.state = {
value: "coconut",
arr: [],
options: [
{ value: "grapefruit", label: "葡萄柚" },
{ value: "lime", label: "酸橙" },
{ value: "coconut", label: "椰子" },
{ value: "mango", label: "芒果" }
]
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(e){
let idx = this.state.arr.findIndex(item=>{
return item === e.target.value
})
if (idx >= 0) {
this.state.arr.splice(idx,1);
} else {
this.state.arr.push(e.target.value);
}
let arr = this.state.arr;
this.setState({arr});
}
render() {
return (
<div>
<select multiple={true} value={this.state.arr} onChange={this.handleChange}>
{this.state.options.map((item,index) => {
return <option value={item.value} key={index}>{item.label}</option>;
})}
</select>
</div>
);
}
}
export default Test4;
4、处理多个输入
当需要处理多个 input
元素时,我们可以给每个元素添加 name
属性,并让处理函数根据 event.target.name
的值选择要执行的操作。
javascript
import React from 'react'
class App extends React.Component {
state = {
firstname: '吴',
lastname: '大勋'
}
handlerChange (e) {
console.log(e.target.name)
this.setState({
[e.target.name]: e.target.value
})
}
render () {
return (
<>
<div>
姓:<input type="text" name="firstname" value={this.state.firstname} onChange = {
this.handlerChange.bind(this)
}/>
</div>
<div>
名:<input type="text" name="lastname" value={this.state.lastname} onChange = {
this.handlerChange.bind(this)
}/>
</div>
<div>
欢迎您: { this.state.firstname } {this.state.lastname }
</div>
</>
)
}
}
export default App
5、文件 input 标签
在 HTML 中,<input type="file">
允许用户从存储设备中选择一个或多个文件,将其上传到服务器,或通过使用 JavaScript 的 File API (FileReader)进行控制。
html
<input type="file" />
因为它的 value 只读,所以它是 React 中的一个非受控组件。将与其他非受控组件在后续文档中一起讨论。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="file" id="banner"/>
<button onclick="getImg()">预览图片</button>
<img src="" id="img" alt="">
</body>
<script>
function getImg(){
const file = document.getElementById('banner').files[0]
console.log(file)
// js 的文件api
const reader = new FileReader()
// 输出 - base64
reader.readAsDataURL(file)
reader.onload = function () {
document.getElementById('img').src = this.result
}
}
</script>
</html>
6、受控输入空值
在受控组件上指定 value 的 prop 会阻止用户更改输入。如果你指定了 value,但输入仍可编辑,则可能是你意外地将value 设置为 undefined 或 null。
下面的代码演示了这一点。(输入最初被锁定,但在短时间延迟后变为可编辑。)
javascript
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
ReactDOM.render(
<input value="hahah" />,
document.getElementById('root')
)
setTimeout(() => {
ReactDOM.render(
<input value={ null } />,
document.getElementById('root')
)
}, 5000)
7、非受控组件
在大多数情况下,我们推荐使用 受控组件 来处理表单数据。在一个受控组件中,表单数据是由 React 组件来管理的。另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理。
要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以 使用 ref 来从 DOM 节点中获取表单数据。
例如,下面的代码使用非受控组件接受一个表单的值:
js
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.jsx'
// ReactDOM.render(
// <App />,
// document.getElementById('root')
// )
ReactDOM.render(
// react的严格模式
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
javascript
// App.jsx - 不推荐写法 严格模式 会有警告信息
import React, { Component } from 'react'
class App extends Component {
getData (e) {
e.preventDefault()
console.log(this.refs.username.value)
}
render () {
// 不推荐这么使用,在严格模式下会爆出警告信息
return (
<form onSubmit = { this.getData.bind(this) }>
<div>
<input type="text" ref="username"/>
</div>
<input type="submit" value="提交"/>
</form>
)
}
}
export default App
jsx
// 推荐写法
import React, { Component } from 'react'
class App extends Component {
usernameRef = React.createRef() // 创建ref
getData (e) {
e.preventDefault()
// 通过this.usernameRef.current拿到DOM节点
console.log(this.usernameRef.current.value)
}
render () {
// 不推荐这么使用,在严格模式下会爆出警告信息
return (
<form onSubmit = { this.getData.bind(this) }>
<div>
<input type="text" ref={ this.usernameRef }/>
</div>
<input type="submit" value="提交"/>
</form>
)
}
}
export default App
因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。
(1) 默认值
在 React 渲染生命周期时,表单元素上的 value
将会覆盖 DOM 节点中的值,在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个 defaultValue
属性,而不是 value
。
javascript
import React, { Component } from 'react'
class App extends Component {
usernameRef = React.createRef() // 创建ref
getData (e) {
e.preventDefault()
console.log(this.usernameRef.current.value)
}
render () {
return (
<form onSubmit = { this.getData.bind(this) }>
<div>
<input type="text" defaultValue="bk2008" ref={ this.usernameRef }/>
</div>
<input type="submit" value="提交"/>
</form>
)
}
}
export default App
同样,<input type="checkbox">
和 <input type="radio">
支持 defaultChecked
,<select>
和 <textarea>
支持 defaultValue
。
(2) 文件输入
在 HTML 中,<input type="file">
可以让用户选择一个或多个文件上传到服务器,或者通过使用 File API 进行操作。
在 React 中,<input type="file">
始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制。
您应该使用 File API 与文件进行交互。下面的例子显示了如何创建一个 DOM 节点的 ref 从而在提交表单时获取文件的信息。
javascript
import React, { Component } from 'react'
class App extends Component {
fileRef = React.createRef()
imgRef = React.createRef()
getData () {
const file = this.fileRef.current.files[0]
console.log(file)
// js 的文件api
const reader = new FileReader()
// 输出 - base64
reader.readAsDataURL(file)
var that = this
reader.onload = function () {
that.imgRef.current.src = this.result
}
}
render () {
return (
<>
<input type="file" ref={ this.fileRef }/>
<button onClick={this.getData.bind(this)}>预览图片</button>
<img src="" ref={ this.imgRef } alt=""></img>
</>
)
}
}
export default App