Appearance
前言
在开发的过程中,我们是少不了表单提交数据这种场景的。而在React中有受控组件和非受控组件都是用来提交表单数据的,他们之间有什么区别呢?
非受控组件
什么样的能被称为非受控组件?
官网是这么定义的:要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以 使用 ref 来从 DOM 节点中获取表单数据。
如果有小伙伴不知道如何使用ref,可以看这篇文章React中的state、props、ref三大属性
俗话就是说,我可以用ref来获取到DOM元素中的值,例如input标签绑定ref通过ref给值赋值,这个是不可控的,这里的不可控并不是人为控制不了,而是说代码控制住不了。我们可以来看一个例子。
tsx
import React, { Component } from "react";
export default class App extends Component {
handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const { username, password } = this;
alert(`你输入的用户名是:${username!.value},你输入的密码是:${password!.value}`);
};
username: HTMLInputElement | null | undefined;
password: HTMLInputElement | null | undefined;
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:
<input ref={(c) => (this.username = c)} defaultValue="admin" name="username" type="text" />
密码:
<input ref={(c) => (this.password = c)} defaultValue="123" type="password" name="password" />
<button>登录</button>
</form>
);
}
}
import React, { Component } from "react";
export default class App extends Component {
handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const { username, password } = this;
alert(`你输入的用户名是:${username!.value},你输入的密码是:${password!.value}`);
};
username: HTMLInputElement | null | undefined;
password: HTMLInputElement | null | undefined;
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:
<input ref={(c) => (this.username = c)} defaultValue="admin" name="username" type="text" />
密码:
<input ref={(c) => (this.password = c)} defaultValue="123" type="password" name="password" />
<button>登录</button>
</form>
);
}
}
从上面代码中可以看出,ref回调函数中,直接将this.username = c
,提交的时候直接使用this.username
,显然我们从输入到提交,代码都是控制不到的,都是你有什么我就拿什么,简直就是一条流水线。不说了,我要收拾东西,准备进厂了。
看着有没有种vue的v-model的样子,你输入我绑定,你改变我也就改变。
受控组件
那啥是受控组件呢?
官网又是这么定义的:渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。
俗话就是,组件的值保存在了state
中,在组件输入过程中发生的改变时,通过onChange
事件获取到输入的值,且只能通过使用 setState()
来更新,组件内的显示的新值
例如我们常用的表单元素<input>
、 <textarea>
和 <select>
input
我们可以来看下一个例子:
ts
import React, { Component } from "react";
export default class example1 extends Component {
state: Readonly<{ username: string }> = {
username: "周星星",
};
render() {
return <input name="username" value={this.state.username} />;
}
}
import React, { Component } from "react";
export default class example1 extends Component {
state: Readonly<{ username: string }> = {
username: "周星星",
};
render() {
return <input name="username" value={this.state.username} />;
}
}
但是这时候你会发现input
的内容是只读的,因为value
会被我们的this.state.username
所控制,当用户输入新的内容时,this.state.username
并不会自动更新,这样的话input
内的内容也就不会变了。
而且在控制台会报错:
大致的意思就是说,你给了个value
值,但是你没有用onChange
去处理,要么你就把value
改成defaultValue
,要么就添加onChange
事件。那我们就跟意思去修改:
ts
import React, { ChangeEvent, Component, FormEvent } from "react";
export default class example1 extends Component {
state: Readonly<{ username: string; password: string }> = {
username: "", //用户名
password: "", //密码
};
handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const { username, password } = this.state;
alert(`你输入的用户名是:${username},你输入的密码是:${password}`);
};
//保存用户名到状态中
saveUsername = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({ username: event.target.value });
};
//保存密码到状态中
savePassword = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({ password: event.target.value });
};
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:
<input onChange={this.saveUsername} type="text" name="username" />
密码:
<input onChange={this.savePassword} type="password" name="password" />
<button>登录</button>
</form>
);
}
}
import React, { ChangeEvent, Component, FormEvent } from "react";
export default class example1 extends Component {
state: Readonly<{ username: string; password: string }> = {
username: "", //用户名
password: "", //密码
};
handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const { username, password } = this.state;
alert(`你输入的用户名是:${username},你输入的密码是:${password}`);
};
//保存用户名到状态中
saveUsername = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({ username: event.target.value });
};
//保存密码到状态中
savePassword = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({ password: event.target.value });
};
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:
<input onChange={this.saveUsername} type="text" name="username" />
密码:
<input onChange={this.savePassword} type="password" name="password" />
<button>登录</button>
</form>
);
}
}
现在不论用户输入什么内容state
与UI
都会跟着更新了,并且我们可以在组件中的其它地方使用this.state.username
来获取到input
里的内容,也可以通过this.setState()
来修改input
里的内容。
对于textarea
标签也和它一样是使用value
和onChange
,这个有兴趣的伙伴可以自行校验
select
上面就举例了一个input的元素的,但对于select
表单元素来说,React
中将其转化为受控组件可能和原生HTML
中有一些区别。
在原生中,我们默认一个select
选项选中使用的是selected
,比如下面这样:
js
import React, { Component } from "react";
export default class example3 extends Component {
render() {
return (
<>
<select>
<option value="grapefruit">周星星</option>
<option value="lime">朱茵</option>
<option selected value="coconut">
李青霞
</option>
<option value="mango">张敏</option>
</select>
</>
);
}
}
import React, { Component } from "react";
export default class example3 extends Component {
render() {
return (
<>
<select>
<option value="grapefruit">周星星</option>
<option value="lime">朱茵</option>
<option selected value="coconut">
李青霞
</option>
<option value="mango">张敏</option>
</select>
</>
);
}
}
但是React 并不会使用 selected
属性,而是在根 select
标签上使用 value
属性。这在受控组件中更便捷,因为您只需要在根标签中更新它。否则控制台就会出现这种报错
正确应该是这么写:
ts
import React, { ChangeEvent, Component, FormEvent } from "react";
export default class example3 extends Component {
state: Readonly<{
value: string;
}> = {
value: "李青霞",
};
handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
this.setState({ value: event.target.value });
};
handleSubmit = (event: FormEvent<HTMLFormElement>) => {
alert("你喜欢的风味是: " + this.state.value);
event.preventDefault();
};
render() {
return (
<>
<form onSubmit={this.handleSubmit}>
<label>
选择你喜欢的风味:
<select value={this.state.value} onChange={this.handleChange}>
<option value="周星星">周星星</option>
<option value="朱茵">朱茵</option>
<option value="李青霞">李青霞</option>
<option value="张敏">张敏</option>
</select>
</label>
<button>提交</button>
</form>
</>
);
}
}
import React, { ChangeEvent, Component, FormEvent } from "react";
export default class example3 extends Component {
state: Readonly<{
value: string;
}> = {
value: "李青霞",
};
handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
this.setState({ value: event.target.value });
};
handleSubmit = (event: FormEvent<HTMLFormElement>) => {
alert("你喜欢的风味是: " + this.state.value);
event.preventDefault();
};
render() {
return (
<>
<form onSubmit={this.handleSubmit}>
<label>
选择你喜欢的风味:
<select value={this.state.value} onChange={this.handleChange}>
<option value="周星星">周星星</option>
<option value="朱茵">朱茵</option>
<option value="李青霞">李青霞</option>
<option value="张敏">张敏</option>
</select>
</label>
<button>提交</button>
</form>
</>
);
}
}
上面的仅仅是单选的,如果我们需要多选的话
- 给
select
标签设置multiple
属性为true
select
标签value
绑定的值为一个数组
例如下面这个例子:
js
import React, { ChangeEvent, Component, FormEvent } from "react";
export default class example3 extends Component {
state: Readonly<{
value: string[];
}> = {
value: ["李青霞"],
};
handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
console.log(event.target.value);
const val = event.target.value;
const oldValue = this.state.value;
const i = this.state.value.indexOf(val);
const newValue = i > -1 ? [...oldValue].splice(i, 1) : [...oldValue, val];
this.setState({ value: newValue });
};
handleSubmit = (event: FormEvent<HTMLFormElement>) => {
alert("你喜欢的是: " + this.state.value);
event.preventDefault();
};
render() {
return (
<>
<form onSubmit={this.handleSubmit}>
<label>
选择你喜欢的:
<select multiple={true} value={this.state.value} onChange={this.handleChange}>
<option value="周星星">周星星</option>
<option value="朱茵">朱茵</option>
<option value="李青霞">李青霞</option>
<option value="张敏">张敏</option>
</select>
</label>
<button>提交</button>
</form>
</>
);
}
}
import React, { ChangeEvent, Component, FormEvent } from "react";
export default class example3 extends Component {
state: Readonly<{
value: string[];
}> = {
value: ["李青霞"],
};
handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
console.log(event.target.value);
const val = event.target.value;
const oldValue = this.state.value;
const i = this.state.value.indexOf(val);
const newValue = i > -1 ? [...oldValue].splice(i, 1) : [...oldValue, val];
this.setState({ value: newValue });
};
handleSubmit = (event: FormEvent<HTMLFormElement>) => {
alert("你喜欢的是: " + this.state.value);
event.preventDefault();
};
render() {
return (
<>
<form onSubmit={this.handleSubmit}>
<label>
选择你喜欢的:
<select multiple={true} value={this.state.value} onChange={this.handleChange}>
<option value="周星星">周星星</option>
<option value="朱茵">朱茵</option>
<option value="李青霞">李青霞</option>
<option value="张敏">张敏</option>
</select>
</label>
<button>提交</button>
</form>
</>
);
}
}
如果你认为到这里就完了的话,那你就错了。
<input type="file" />
总会一些会跳出三界外的,譬如我们input标签type
属性为file
时,它是个非受控组件。
对于file类型的表单控件它始终是一个不受控制的组件,因为它的值只能由用户设置,而不是以编程方式设置。
例如我现在想要通过状态更新来控制它:
ts
import React, { ChangeEvent, Component, FormEvent } from "react";
export default class example4 extends Component {
state: Readonly<{ files: [] }> = {
files: [],
};
handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
handleFile = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.files);
const files = [...e.target.files!];
console.log(files);
this.setState({
files,
});
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="file" value={this.state.files} onChange={(e) => this.handleFile(e)} />
<button>提交</button>
</form>
);
}
}
import React, { ChangeEvent, Component, FormEvent } from "react";
export default class example4 extends Component {
state: Readonly<{ files: [] }> = {
files: [],
};
handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
handleFile = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.files);
const files = [...e.target.files!];
console.log(files);
this.setState({
files,
});
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="file" value={this.state.files} onChange={(e) => this.handleFile(e)} />
<button>提交</button>
</form>
);
}
}
在选择了文件之后,我试图用setState
来更新,结果却报错了:
所以我们应当使用非受控组件的方式来获取它的值,可以这样写:
ts
import React, { ChangeEvent, Component, FormEvent } from "react";
export default class example4 extends Component {
fileRef = React.createRef<HTMLInputElement>();
state: Readonly<{ files: [] }> = {
files: [],
};
handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log("我们可以获得file的值为", this.fileRef.current!.files);
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="file" ref={this.fileRef} />
<button>提交</button>
</form>
);
}
}
import React, { ChangeEvent, Component, FormEvent } from "react";
export default class example4 extends Component {
fileRef = React.createRef<HTMLInputElement>();
state: Readonly<{ files: [] }> = {
files: [],
};
handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log("我们可以获得file的值为", this.fileRef.current!.files);
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="file" ref={this.fileRef} />
<button>提交</button>
</form>
);
}
}
这里获取到的files
是一个数组哈,当然,如果你没有开启多选的话,这个数组的长度始终是1
,开启多选也非常简单,只需要添加multiple
属性即可:
js
<input type="file" multiple ref={this.fileRef} />
<input type="file" multiple ref={this.fileRef} />
总结
OK,相信大家对这两组概念已经有了一个清晰的认识。什么?你问我实际的应用场景?
嗯...这个用React官方的话来说,绝大部分时候推荐使用受控组件
来实现表单,因为在受控组件中,表单数据由React
组件负责处理;当然如果选择非受控组件
的话,表单数据就由DOM
本身处理。