本文将详细介绍 React 中的表单处理方式,包括受控组件和非受控组件的概念、实现方法及其优缺点。通过本文,读者将能够更好地理解和使用 React 中的表单处理机制,提升开发效率和用户体验。
在 React 里,HTML 表单元素的工作方式和其他的 DOM 元素有些不同,这是因为表单元素通常会保持一些内部的 state。例如这个纯 HTML 表单只接受一个名称:
- <form>
- <label>
- 名字:
- <input type="text" name="name" />
- </label>
- <input type="submit" value="提交" />
- </form>
-
此表单具有默认的 HTML 表单行为,即在用户提交表单后浏览到新页面。如果你在 React 中执行相同的代码,它依然有效。但大多数情况下,使用 JavaScript 函数可以很方便的处理表单的提交, 同时还可以访问用户填写的表单数据。实现这种效果的标准方式是使用“受控组件”。
在 HTML 中,表单元素(如、 和 )通常自己维护 state,并根据用户输入进行更新。
而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用setState()来更新。
我们可以把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作-onChange。
被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。
例如,如果我们想让前一个示例在提交时打印出名称,我们可以将表单写为受控组件:
- 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 元素,或者通过其他事件处理函数重置,但这意味着你需要编写更多的代码。
在 HTML 中, <textarea> 元素通过其子元素定义其文本:
- <textarea>
- 你好, 这是在 text area 里的文本
- </textarea>
-
而在 React 中,<textarea> 使用 value 属性代替。这样,可以使得使用 <textarea> 的表单和使用单行 input 的表单非常类似:
- 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 初始化于构造函数中,因此文本区域默认有初值。
在 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 属性。这在受控组件中更便捷,因为您只需要在根标签中更新它。例如:
- 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']}>
参考 — 不讲解
- 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;
-
当需要处理多个 input 元素时,我们可以给每个元素添加 name 属性,并让处理函数根据 event.target.name 的值选择要执行的操作。
- 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
-
在 HTML 中,<input type="file"> 允许用户从存储设备中选择一个或多个文件,将其上传到服务器,或通过使用 JavaScript 的 File API (FileReader)进行控制。
- <input type="file" />
-
因为它的 value 只读,所以它是 React 中的一个非受控组件。将与其他非受控组件在后续文档中一起讨论。
- <!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>
-
在受控组件上指定 value 的 prop 会阻止用户更改输入。如果你指定了 value,但输入仍可编辑,则可能是你意外地将value 设置为 undefined 或 null。
下面的代码演示了这一点。(输入最初被锁定,但在短时间延迟后变为可编辑。)
- // 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)
-
在大多数情况下,我们推荐使用 受控组件 来处理表单数据。在一个受控组件中,表单数据是由 React 组件来管理的。另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理。
要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以 使用 ref 来从 DOM 节点中获取表单数据。
例如,下面的代码使用非受控组件接受一个表单的值:
- // 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')
- )
-
- // 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
-
- // 推荐写法
- 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 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。
在 React 渲染生命周期时,表单元素上的 value 将会覆盖 DOM 节点中的值,在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个 defaultValue 属性,而不是 value。
- 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。
在 HTML 中,<input type="file"> 可以让用户选择一个或多个文件上传到服务器,或者通过使用 File API 进行操作。
在 React 中,<input type="file"> 始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制。
您应该使用 File API 与文件进行交互。下面的例子显示了如何创建一个 DOM 节点的 ref 从而在提交表单时获取文件的信息。
- 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
-