从零开始的react入门教程(五),了解react中的表单,何为受控组件与非受控组件

壹 ❀ 引

我们在从零开始的react入门教程(四),了解常用的条件渲染、列表渲染与独一无二的key一文中介绍了react中常用的条件渲染操作,比如三元运算符,逻辑运算符等,结合react组件或者react元素,我们能做到很多视图层的切换效果。

除此之外我们也介绍了react中的渲染操作,不同于vue类似框架使用循环指令,react中直接使用数组API达到渲染元素元素块的操作;说到循环我们不得不提到关键属性key,为循环的元素提供独一无二的key有利于react后续的DOM更新,当然,尽可能不要使用数组索引作为key,会造成什么问题我们也有在上文举例说明。

那么这篇文章,我们将围绕react表单展开,除了介绍常用表单用法,我们还会介绍react中受控组件与非受控组件的概念,这对于我们在项目开发中,如何设计自己的组件会有一定启发,本文开始。

贰 ❀ 从表单说起

对于传统HTML所提供的比如inputselect等表单元素,它们都会自己维护内部的state。说通俗点,我们修改input的值,再获取这个input的value,你会发现value已经被同步更新了,这个操作由表单元素内部自己完成,我们并没有干涉。

<input type="text" class="echoInput">
输入的内容是:
<span class="echoSpan"></span>
const input = document.querySelector('.echoInput');
const span = document.querySelector('.echoSpan');
input.onchange = function () {
    span.innerHTML = input.value;
}

比如上述例子,我们使用onchange事件单纯监听了input值的变化,并未对input做赋值操作,修改value的行为由input自身完成。

(注意:原生change事件并不是改了值就会触发,而是修改值并失去焦点才会触发)

如果你有了解过vue或者angularjs类似的框架你会发现实现做法上有所不同。说到表单,你可能第一想到的是双向绑定,以及类似v-modelng-model类似的指令;以vue为例,相对传统表单表vue自行做了部分指令封装,通过为input添加v-model指令并提供一个变量作为初始值,之后不管我们修改变量还是input都会影响另一方,input修改相对原生change还要失焦才能感知变化这点上也人性化了很多。

<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>

叁 ❀ react中的表单

在前面的文章中我们已经得知react属于单向数据流,props与state组成的数据像瀑布的水流一样从上往下传递,而类似v-model的指令是接受一个变量同时,也拥有反向修改变量的权力,这与react中props应该只读,state只能通过setState修改的特性有所违背。

不过不用担心,对此问题我们已经介绍过了对应的解决方案,如果你想在子组件中修改上层传递的props,请在传递数据的同时一并提供修改数据的方法,如果你想更新内部state请使用setState方法,所以到这里你应该知晓react中如何操控表单元素了。

叁 ❀ 壹 input-text

让我们来看个例子,从最简单的input说起:

class EchoInput extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = { value: '' };
    }

    handleChange = (event) => {
        // 还记得e对象吗,这里就通过e获取当前元素的value
        this.setState({ value: event.target.value });
    }

    render() {
        return (
            <form className="echo">
                <label>
                    名字:
                    <input type="text" value={this.state.value} onChange={this.handleChange} />
                </label>
                <div>{'提交的名字: ' + this.state.value}</div>
            </form>
        );
    }
}

ReactDOM.render(<EchoInput />, document.getElementById('root'))

在上述例子中,对于input而言state是它唯一的数据来源,由于onChange监听了input的值变化,只要用户修改input值就会触发handleChange中的setState去更新state中的value属性。

叁 ❀ 贰 input-radio

还记得原生的input如何做单选吗吗?如果你想让多个input表示单选,你需要将input的type类型设置为radio,同时它们需要拥有相同的name属性。现在让我们看看在react表单中如何做单选,直接上例子:

class EchoInput extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = { value: '' };
    }

    handleChange = (event) => {
        // 还记得e对象吗,这里就通过e获取当前元素的value
        this.setState({ value: event.target.value });
    }

    render() {
        return (
            <form className="echo">
                请选择你的性别:
                <label>
                   男
                   <input
                       type="radio"
                       value="男性"
                       checked={this.state.value==='男性'}
                       onChange={this.handleChange}/>
               </label>
                <label>
                    女
                    <input
                        type="radio"
                        value="女性"
                        checked={this.state.value==='女性'}
                        onChange={this.handleChange}/>
                </label>
                <div>{'选择的性别为: ' + this.state.value}</div>
            </form>
        );
    }
}

ReactDOM.render(<EchoInput />, document.getElementById('root'))

与普通的input text类型不同,这里我们为每个input直接提供了value值,并通过checked属性判断当前input是否应该被选中。当用户点击不同单选按钮时,通过change事件去更新state中的value,上述代码中,由于state的值总是只能为其中一个,所以我们没加name属性也达到了单选的目的。

叁 ❀ 叁 input-checkbox

既然都说了单选,我们顺便说说多选checkbox如何使用,直接上代码:

class EchoInput extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = { heroes: [{ name: '曜', id: 0, checked: false }, { name: '澜', id: 1, checked: false }, { name: '盾山', id: 3, checked: false }] };
    }

    handleChange = (index,e) => {
        let heroes = [...this.state.heroes];
        heroes[index].checked = !heroes[index].checked;
        this.setState({ heroes:heroes });
    }

    render() {
        return (
            <form className="echo">
                选择你喜欢的英雄:
                {
                    this.state.heroes.map((hero, index) => {
                        return (
                            <label key={hero.id}>
                                {hero.name}
                                <input type="checkbox" checked={hero.checked} onChange={this.handleChange.bind(this,index)} />
                            </label>
                        )
                    })
                }
            </form>
        );
    }
}

ReactDOM.render(<EchoInput />, document.getElementById('root'))

我们通过遍历,得到了一个checkbox的列表,在点击选择时,我们通过index找到对应的数据,将其checked设置为当前反值,从而达到了选中与反选的效果。

叁 ❀ 肆 textarea

我们知道原生的textarea标签有两种设置值的做法,因为textarea有结束标签,所以可以通过innerHTML设置,其次也支持像input那样通过value设置,一般我们推荐通过value管理textarea的值,因为这样我们就能像input-text一样管理状态变化了,直接上例子:

class EchoInput extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = { value: '请输入内容' };
    }

    handleChange = (event) => {
        this.setState({ value: event.target.value });
    }

    render() {
        return (
            <form className="echo">
                <label>
                    <textarea type="text" value={this.state.value} onChange={this.handleChange} />
                </label>
                <div>{'输入的内容为: ' + this.state.value}</div>
            </form>
        );
    }
}

ReactDOM.render(<EchoInput />, document.getElementById('root'))

叁 ❀ 伍 select

让我们再来看一看select的例子,比如这是一个听风是风曾用网名的列表:

const names = [{ id: 0, value: '听风是风'}, { id: 1, value: '时间跳跃'}, { id: 2, value: '行星飞行'}]
class EchoList extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = { name: '' };
    }

    handleChange = (event) => {
        this.setState({ name: event.target.value });
    }

    render() {
        const list = this.props.names.map(name => (
            <option value={name.value} key={name.id}>{name.value}</option>
        ))
        return (
            <form className='echo'>
                <label>
                    听风是风的名字列表:
                    <select value={this.state.name} onChange={this.handleChange}>
                        {list}
                    </select>
                </label>
                <div>当前选择的是:{this.state.name}</div>
            </form>
        );
    }
}

ReactDOM.render(<EchoList names={names} />, document.getElementById('root'))

在上述例子中,我们借用了循环生成了option选项列表,value属性与onChange属性均添加在select标签上。

有同学可能就要问,如果我要给select添加默认值怎么办,传统select默认值通过selected属性实现,不过这里很简单,我们只需要在constructor中定义state时给予一个默认值即可,修改上述代码中的state初始化为:

constructor(props) {
    super(props);
    this.state = { name: '行星飞行' };
}

刷新页面,你会发现select的选项已经默认选中了行星飞行。当然,select可以通过multiple属性让其支持多选,我们可以在select上添加该属性,并修改state的值为数组,这里只列出需要修改的部分:

constructor(props) {
    super(props);
    this.state = { name: ['听风是风', '行星飞行'] };
}

<select value={this.state.name} onChange={this.handleChange} multiple={true}>
    {list}
</select>

叁 ❀ 受控组件与非受控组件

在上文介绍的表单中有一个通性,所有表单元素的value都由state提供,同时当值变化时也是通过setState去更新state中对应的值,对于这些组件它们,它们就像工具人一般存在,自身的值与行为都受外部控制,所以这类组件在react中被称为受控组件。总结来说,这类组件的value是受控制的,它接受外部传递的state属性作为值,以及一个操控该值的方法。

那么既然有受控组件,是不是也有非受控组件呢?那必须有,与受控组件最直接的区别是,非受控组件可以接受外部传递的一个默认值,但除此之外,非受控组件会自己管理值的状态变化(就像文章开头我们说的原生input标签),获取非受控组件的value不再是通过state,而是由DOM节点提供,我们来看个例子:

class NameForm extends React.Component {
    constructor(props) {
        super(props);
        this.input = React.createRef();
    }
    handleClick = (event) => {
        console.log('输入的内容为: ' + this.input.current.value);
    }

    render() {
        return (
            <label>
                Name:
                <input type="text" defaultValue={'我是默认值'} ref={this.input} />
                <input type="button" value="Submit" onClick={this.handleClick} />
            </label>
        )
    }
}

ReactDOM.render(<NameForm />, document.getElementById('root'))

在上述例子中,我们通过defaultValue属性为组件提供默认值,除此之外,你会发现input的变化与state没有任何关系,input的vaule如何变化由input组件自身管理,那这就是一个非受控组件的例子。

上文中我们通过ref直接操作dom并获取了dom的值,如果你有了解的vue,应该不会对此属性陌生。事实上,不管是vue还是react,子组件获取父组件的属性可以通过属性传递,有些特殊的场景,父组件可能也要获取子组件的属性或者方法,那么ref就能解决这类问题。这里只是简单提及,具体用法我们会在后面具体解释,先别急。

回归正题,对于非受控组件而言,你会发现对比受控组件,你不需要额外提供控制state变化的监听方法,因为value变化组件自身就已经帮你做了。

在前面介绍input的类型中,其实我们少介绍了一种类型,也就是file类型,它用于上传文件,而这种类型的input也是典型的非受控组件。因为说直白点,你根本控制不了它的value,用户上传什么那么它的vaule就是什么,你无法通过方法手动去修改它的值,因为file类型的input的值为只读。

class FileInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.fileInput = React.createRef();
  }
  handleSubmit(event) {
    event.preventDefault();
    console.log(
      `选择的文件为 - ${this.fileInput.current.files[0].name}`
    );
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Upload file:
          <input type="file" ref={this.fileInput} />
        </label>
        <br />
        <button type="submit">Submit</button>
      </form>
    );
  }
}

ReactDOM.render(
  <FileInput />,
  document.getElementById('root')
);

这是官网提供的一个例子,我们并未为input绑定value或者提供defaultValue,input内部值的变化完全由自身管理。

准备来说,使用受控组件或者受控组件都是合理的,但至少你在设计组件之前,应该明确你的组件应该设计成哪种形式,因为在我目前的公司,就有人定义非受控组件,但又提供监听并改变值的方法,这会让人混乱,至少这种做法是非常不好的。

肆 ❀ 总

好了,那么到这里,我们介绍了react表单中几种常见的表单元素操作,但事实上,你需要手写表单组件的场景可能并不会很多,因为往往我们可能会使用到其它三方UI组件库,但即便如此,了解react中的原生做法也是有必要的,至少在日常开发中,遇到一些场景你能明白这些组件的value大致是如何运作的。

初次之外,我们简单提及了react中受控组件与非受控组件的概念,任何东西都没有绝对意义上的好与坏,在合适的场景使用合适的做法那就是优秀的,当然我们也说了,至少别写出受控与非受控的混合体,这极不优雅。

下周就是圣诞节了,时间过得很快,不知不觉2020年都到尾声了,心情还是有点激动啊,期待下周的来临,那么本文结束。

原文地址:https://www.cnblogs.com/echolun/p/14165531.html