Skip to content

前言

在开发的过程中,我们是少不了表单提交数据这种场景的。而在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内的内容也就不会变了。

而且在控制台会报错:

image.png

大致的意思就是说,你给了个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>
    );
  }
}

现在不论用户输入什么内容stateUI都会跟着更新了,并且我们可以在组件中的其它地方使用this.state.username来获取到input里的内容,也可以通过this.setState()来修改input里的内容。

对于textarea标签也和它一样是使用valueonChange,这个有兴趣的伙伴可以自行校验

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 属性。这在受控组件中更便捷,因为您只需要在根标签中更新它。否则控制台就会出现这种报错

image.png

正确应该是这么写:

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来更新,结果却报错了:

image.png

所以我们应当使用非受控组件的方式来获取它的值,可以这样写:

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} />

总结

20220225_pic_0.gif

OK,相信大家对这两组概念已经有了一个清晰的认识。什么?你问我实际的应用场景?

嗯...这个用React官方的话来说,绝大部分时候推荐使用受控组件来实现表单,因为在受控组件中,表单数据由React组件负责处理;当然如果选择非受控组件的话,表单数据就由DOM本身处理。