React 表单详解

前言

本文将详细介绍 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
相关推荐
ZVAyIVqt0UFji33 分钟前
go-zero负载均衡实现原理
运维·开发语言·后端·golang·负载均衡
loop lee36 分钟前
Nginx - 负载均衡及其配置(Balance)
java·开发语言·github
Cachel wood40 分钟前
Vue.js前端框架教程8:Vue消息提示ElMessage和ElMessageBox
linux·前端·javascript·vue.js·前端框架·ecmascript
SomeB1oody1 小时前
【Rust自学】4.1. 所有权:栈内存 vs. 堆内存
开发语言·后端·rust
toto4121 小时前
线程安全与线程不安全
java·开发语言·安全
PP东1 小时前
ES6学习Generator 函数(生成器)(八)
javascript·学习·es6
水木流年追梦2 小时前
【python因果库实战10】为何需要因果分析
开发语言·python
桃园码工2 小时前
4_使用 HTML5 Canvas API (3) --[HTML5 API 学习之旅]
前端·html5·canvas
桃园码工2 小时前
9_HTML5 SVG (5) --[HTML5 API 学习之旅]
前端·html5·svg
人才程序员3 小时前
QML z轴(z-order)前后层级
c语言·前端·c++·qt·软件工程·用户界面·界面