React 受控与非受控组件

react.jpg

refs 的使用

在 React 的开发模式中, 通常情况下不需要、也不建议直接操作 DOM 元素, 但是某些特殊的情况, 确实需要获取到 DOM 进行某些操作, 例如

  • 管理焦点、文本选择、媒体播放等
  • 触发强制动画
  • 集成第三方 DOM 库

创建 ref 的方式

如何创建 refs 来获取对应的 DOM 呢? 目前有三种方式

  1. 传入字符串
    • 使用时通过this.refs.传入的字符串格式获取对应的元素
  2. 传入对象
    • 对象通过React.createRef()方式创建
    • 使用时获取到创建的对象其中有一个current属性就是对应的元素
  3. 传入函数
    • 该函数会在 DOM 被挂载时进行回调, 这个函数会传入一个元素对象, 我们可以自己保存
    • 使用时, 直接拿到之前报错的元素对象即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { PureComponent, createRef } from 'react'

export default class App extends PureComponent {
constructor(props) {
super(props)

this.titleRef = createRef()
this.titleEl = null
}

render() {
return (
<div>
<h2 ref="title">String Ref</h2>
<h2 ref={this.titleRef}>Hello Create Ref</h2>
<h2 ref={element => this.titleEl = element}>Callback Ref</h2>

<button onClick={e => this.changeText()}>改变文本</button>
</div>
)
}

changeText() {
this.refs.title.innerHTML = 'String Ref Change'
this.titleRef.current.innerHTML = 'Object Ref Change'
this.titleEl.innerHTML = 'Function Ref Change'
}
}

ref 节点的类型

ref的值根据节点的类型而有所不同

  • ref属性用于 HTML 元素时, 构造函数中使用React.createRef()创建的ref接收底层 DOM 元素作为其current属性
  • ref属性用于自定义 Class 组件时, ref对象接收组件的挂载实例作为其current属性
  • 你不能在函数组件上使用ref属性, 因为它们没有实例

NfIpQI.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import React, { PureComponent, createRef } from 'react'

class Counter extends PureComponent {
constructor(props) {
super(props)

this.state = {
counter: 0
}
}

render() {
return (
<div>
<h2>当前计数: {this.state.counter}</h2>
<button onClick={e => this.increment()}>+1</button>
</div>
)
}

increment() {
this.setState({
counter: this.state.counter + 1
})
}
}

export default class App extends PureComponent {
constructor(props) {
super(props)

this.counterRef = createRef()
}

render() {
return (
<div>
<Counter ref={this.counterRef} />
<button onClick={e => this.increment()}>app +1</button>
</div>
)
}

increment() {
this.counterRef.current.increment()
}
}

上面的代码中, 在App组件中ref属性用于Counter组件上, 所以ref对象的current属性为Counter组件实例, 从而可以通过App组件调用Counter组件的increment方法

函数式组件没有实例, 所以无法通过ref获取它们的实例

  • 但是某些时候, 我们可能想要获取函数式组件中的某个 DOM 元素
  • 这个时候我们可以通过React.forwardRef

受控组件

认识受控组件

默认提交表单方式

React中, HTML 表单的处理方式和普通的 DOM 元素不太一样, 表单元素通常会保存一些内部的state

比如下面的 HTML 表单元素

1
2
3
4
5
6
7
<form>
<label>
名字:
<input type="text" name="name" />
</label>
<input type="submit" value="提交" />
</form>
  • 这个处理方式是 DOM 默认处理 HTML 表单的行为, 在用户点击提交时会提交到某个服务器上, 并且刷新页面
  • 在 React 中, 并没有禁止这个行为, 它依然是有效的
  • 但是通常情况下会使用 JavaScript 函数来方便的处理表单提交, 同时还可以访问用户填写的表单数据
  • 实现这种效果的标准方式是使用“受控组件”

受控组件提交表单

在 HTML 中, 表单元素(如<input><textarea><select>)通常自己维护state, 并根据用户输入进行更新

而在 React 中, 可变状态(mutable state)通常保存在组件的state属性中, 并且只能通过使用setState()来更新

  • 将两者结合起来, 使Reactstate成为“唯一数据源”
  • 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作
  • 被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”

例如, 如果我们想让前一个示例在提交时打印出用户名, 那么就可以将表单改写为受控组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import React, { PureComponent } from 'react'

export default class App extends PureComponent {
constructor(props) {
super(props)

this.state = {
username: ''
}
}

render() {
const { username } = this.state

return (
<div>
<form onSubmit={e => this.handleSubmit(e)}>
<label htmlFor="username">
用户名:
<input
type="text"
id="username"
onChange={e => this.handleUsernameChange(e)}
value={username}
/>
</label>
<input type="submit" value="提交" />
</form>
</div>
)
}

handleUsernameChange(event) {
this.setState({
username: event.target.value
})
}

handleSubmit(event) {
console.log(this.state.username)
event.preventDefault()
}
}

由于在表单元素上设置了value属性, 因此显示的值将始终为this.state.username, 这使得 React 的state成为唯一数据源

由于handleUsernameChange在每次输入时都会执行并更行 React 的state, 因此显示的值将随着用户输入而更新

常见表单的处理

ElementValue propertyChange callbackNew value in the callback
<input type="text" />value="string"onChangeevent.target.value
<input type="checkbox" />checked={boolean}onChangeevent.target.checked
<input type="radio" />value="string"onChangeevent.target.checked
<textarea />value="string"onChangeevent.target.value
<select />value="option value"onChangeevent.target.value

textarea 标签

textarea标签和input比较相似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import React, { PureComponent } from 'react'

export default class App extends PureComponent {
constructor(props) {
super(props)

this.state = {
article: '请编写你喜欢的文章'
}
}

render() {
return (
<div>
<form onSubmit={e => this.handleSubmit(e)}>
<label htmlFor="article">
<textarea
id="article"
cols="30"
rows="10"
value={this.state.article}
onChange={e => this.handleArticelChange(e)}
/>
</label>
<div>
<input type="submit" value="发布文章" />
</div>
</form>
</div>
)
}

handleArticelChange(event) {
this.setState({
article: event.target.value
})
}

handleSubmit(event) {
console.log(this.state.article)
event.preventDefault()
}
}

select 标签

select标签的使用也非常简单, 只是它不需要通过selected属性来控制哪一个被选中, 它可以匹配statevalue来选中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import React, { PureComponent } from 'react'

export default class App extends PureComponent {
constructor(props) {
super(props)

this.state = {
fruits: 'orange'
}
}

render() {
return (
<div>
<form onSubmit={e => this.handleSubmit(e)}>
<label htmlFor="fruits">
<select
id="fruits"
value={this.state.fruits}
onChange={e => this.handleFruitsChange(e)}
>
<option value="apple">苹果</option>
<option value="orange">橘子</option>
<option value="banana">香蕉</option>
</select>
</label>
<div>
<input type="submit" value="提交" />
</div>
</form>
</div>
)
}

handleFruitsChange(event) {
this.setState({
fruits: event.target.value
})
}

handleSubmit(event) {
console.log(this.state.article)
event.preventDefault()
}
}

处理多个输入

多处理方式可以像单处理方式那样进行操作, 但是需要多个监听方法

  • 这里我们可以使用 ES6 的一个语法: 计算属性名(Computed property names)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let i = 0
let a = {
['foo' + ++i]: i,
['foo' + ++i]: i,
['foo' + ++i]: i
}

console.log(a.foo1) // 1
console.log(a.foo2) // 2
console.log(a.foo3) // 3

let param = 'size'
let config = {
[param]: 12,
['mobile' + param.charAt(0).toUpperCase() + param.slice(1)]: 4
}

console.log(config) // {size: 12, mobileSize: 4}

对应的代码示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import React, { PureComponent } from 'react'

export default class App extends PureComponent {
constructor(props) {
super(props)

this.state = {
username: '',
password: ''
}
}

render() {
const { username, password } = this.state

return (
<div>
<form onSubmit={e => this.handleSubmit(e)}>
<label htmlFor="username">
用户:
<input
type="text"
id="username"
name="username"
onChange={e => this.handleChange(e)}
value={username}
/>
</label>
<label htmlFor="password">
密码:
<input
type="text"
id="password"
name="password"
onChange={e => this.handleChange(e)}
value={password}
/>
</label>
<input type="submit" value="提交" />
</form>
</div>
)
}

handleChange(event) {
this.setState({
[event.target.name]: event.target.value
})
}

handleSubmit(event) {
console.log(this.state.username, this.state.password)
event.preventDefault()
}
}

非受控组件

React 推荐大多数情况下使用受控组件来处理表单数据

  • 一个受控组件中, 表单数据是由 React 组件来管理的
  • 另一种替代方案是使用非受控组件, 这时表单数据将交由 DOM 节点来处理

如果要使用非受控组件中的数据, 那么我们需要使用ref来从 DOM 节点中获取表单数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React, { PureComponent, createRef } from 'react'

export default class App extends PureComponent {
constructor(props) {
super(props)

this.usernameRef = createRef()
}

render() {
return (
<div>
<form onSubmit={e => this.handleSubmit(e)}>
<label htmlFor="">
用户:
<input
defaultValue="username"
type="text" name="username"
ref={this.usernameRef}
/>
</label>
<input type="submit" value="提交" />
</form>
</div>
)
}

handleSubmit(event) {
event.preventDefault()
console.log(this.usernameRef.current.value)
}
}
  • 使用ref来获取input元素
  • 在非受控组件中通常使用defaultValue来设置默认值

同样, <input type="checkbox" /><input type="radio" />支持defaultChecked, <select /><textarea />支持defaultValue

-------------本文结束感谢阅读-------------