阅读 1405

在实际业务中如何灵活运用受控组件与非受控组件

概况

在 web 开发中经常会用表单来提交数据, react 中实现表单主要使用两种组件:受控和非受控。两者的区别就在于组件内部的状态是否是全程受控的。受控组件的状态全程响应外部数据的变化,而非受控组件只是在初始化的时候接受外部数据,然后就自己在内部维护状态了。这样描述可能比较抽象,下面通过 demo 来看一下具体的怎么书写。

受控组件

原生组件还有一些公用组件库都有一些通用的实践,即用于表单的组件一般都暴露出两个数据接口: value,defaultValue。如果指定了value,那么这个组件就被控制了,时刻响应父组件中数据的变化。例如
<DatePicker value={this.state.time} onChange={this.onChange} />
下面以 antd-design 举例

import React, { Component } from 'react';
import { DatePicker } from 'antd';
import 'antd/dist/antd.css';

class Form extends Component{
  constructor(props){
    super(props)
    this.state = {
      time: ""
    }
    this.onChange.bind(this);
  }

  onChange(value){
    this.setState({
      time: value
    });
  }
  render(){
    return(
      <div>
        <DatePicker value={this.state.time} onChange={this.onChange} />
      </div>
    )
  }
}
复制代码

非受控组件

如果只是指定了 defaultValue ,那么这个组件就是非受控的,只是在初始化的时候指定一下初始值,随后就交出了控制权。就像这样:
<DatePicker defaultValue={this.state.time} ref={(input) => this.input = input} />
具体看下面的例子:

import React, { Component } from 'react';
import { DatePicker } from 'antd';
import 'antd/dist/antd.css';

class Form extends Component{
  constructor(props){
    super(props)
    this.state = {
      time: ""
    }
    this.handleSubmit.bind(this);
  }

  handleSubmit(){
    console.log(this.input.value);
  }
  
  render(){
    return(
      <div>
        <DatePicker defaultValue={this.state.time} ref={(input) => this.input = input} />
        <div class="submit" onClick={ handleSubmit }/>
      </div>
    )
  }
}
复制代码

具体在业务中的新增和编辑应该怎么写?

在实际业务中具体使用哪种方式来实现表单就得看需求。我引用了一张图片很好的说明了这两个组件的使用场景:
image

大致来说,当仅仅需要一次性收集数据,提交时菜需要验证,用两种方式都可以。
但是,当业务中需要对数据进行即时校验,格式化输入数据等需求,就只能使用受控组件了。毕竟受控组件能力更强。
让我们更进一步,实际涉及表单的业务(也就是我们常说的 CRUD )中应该怎样运用呢?下面展示一下。

新增表单

比如研发的小锅从产品经理小帅那接到一个需求“需要收集两个时间数据,开始时间和结束时间,开发时间比较紧张”。
小明想了一下解决方案,只是收集数据而已,而且时间紧张,肯定是选择非受控,尽快搞定才是王道。三两下小锅就写出了以下代码,运行也看出来没什么问题。

import React, { Component } from 'react';
import { DatePicker } from 'antd';
import 'antd/dist/antd.css';
import moment from 'moment';

class Form extends Component{
  constructor(props){
    super(props)
    this.state = {
      // 开始时间
      startTime: moment('12:08:23', 'HH:mm:ss'),
      // 结束时间
      endTime: moment('13:08:23', 'HH:mm:ss'),
    }
    this.handleSubmit.bind(this);
  }

  handleSubmit(){
    console.log(this.input.value);
    let startTime = this.start.value;
    let endTime = this.end.value;
    // 提交数据
    createForm(
      {
        startTime,
        endTime
      }
    ).then(function() {
      // do sth
    })
  }

  render(){
    return(
      <div>
        <div>
          <DatePicker
            showTime
            format="HH:mm:ss"
            defaultValue={this.state.startTime}
            ref={(input) => this.start = input}
          />
          <DatePicker
            showTime
            format="HH:mm:ss"
            value={endValue}
            defaultValue={this.state.endTime}
            ref={(input) => this.start = input}
          />
        </div>
        <div class="submit" onClick={ handleSubmit }/>
      </div>
    )
  }
}
复制代码

激动的小锅叫来小帅观摩一下,小帅看了一眼,突然觉得少了点什么。“对,忘了说了,开始时间不能大于结束时间,这里还需要加个实时的验证,我认为应该就一两行代码就可以搞定了”。小锅也是突然想到了这个 bug,心想这是两三行代码可以搞定的吗?心里一阵苦笑。

使用受控组件的新增表单

小锅仔细思考了一下,既然是要实时验证,必须得受控组件才行,每次在输入的时间结束之后都会输入的时间和另一个组件的时间戳进行大小对比就可以了。现在需求比较简单,虽然是重构但是改动量还不大。小锅立刻写起来。

import React, { Component } from 'react';
import { DatePicker } from 'antd';
import 'antd/dist/antd.css';
import moment from 'moment';

class Form extends Component{
  constructor(props){
    super(props)
    this.state = {
      // 开始时间
      startTime: moment('12:08:23', 'HH:mm:ss'),
      // 结束时间
      endTime: moment('13:08:23', 'HH:mm:ss'),
    }
    this.onStartChange.bind(this);
    this.onEndChange.bind(this);
  }

  onStartChange(value){
    // displayTimestamp 自定义的将 moment 对象生成时间戳的方法
    if(this.state.endTime){
      let endValue = displayTimestamp(this.state.endTime);
      let startValue = displayTimestamp(value);
      // 比较两个时间的时间戳
      if(startValue > endValue){
        return ;
      }
    }
    else {
      this.setState({
        startTime: value
      })
    }
  }
  onEndChange(value){
    // displayTimestamp 自定义的将 moment 对象生成时间戳的方法
    if(this.state.startTime){
      let endValue = displayTimestamp(value);
      let startValue = displayTimestamp(this.state.startTime);
      if(startValue > endValue){
        return ;
      }
    }
    else {
      this.setState({
        endTime: value
      })
    }
  }

  handleSubmit(){
    // formatTime 为自定义的时间格式化函数
    let startTime = formatTime(this.state.startTime);
    let endTime = formatTime(this.state.endTime);
    // 提交数据
    createForm(
      {
        startTime,
        endTime
      }
    ).then(function() {
      // do sth
    })
  }

  render(){
    return(
      <div>
        <div>
          <DatePicker
            showTime
            format="HH:mm:ss"
            value={this.state.startTime}
            onChange={this.onStartChange}
          />
          <DatePicker
            showTime
            format="HH:mm:ss"
            value={this.state.endTime}
            onChange={this.onEndChange}
          />
        </div>
        <div class="submit" onClick={ handleSubmit }/>
      </div>
    )
  }
}

复制代码

编辑表单

小锅又把产品经理叫了过来,小帅点了几下,觉得没问题了,说“现在又有了一个需求,这个表单的编辑也实现出来吧”
小锅想了一下,编辑表单无非就是接受外部参数的变化,接受参数的来源具体有两个地方:初始化,请求的数据返回的时候。也不是很难,于是小锅就爽快地接下了这个任务。
如果不考虑使用 redux 的情况下,小锅想也就是三个地方需要改一下。

constructor:
// 初始化赋值新增表单 id
  constructor(props){
    super(props)
    this.state = {
      // id 为表单 id
      id: this.props.id,
      // 开始时间
      startTime: moment('12:08:23', 'HH:mm:ss'),
      // 结束时间
      endTime: moment('13:08:23', 'HH:mm:ss'),
    }
    this.onStartChange.bind(this);
    this.onEndChange.bind(this);
  }

componentDidMount:
// 新增获取表单数据的部分,获取成功之后为表单赋值
  componentDidMount(){
    // 获取表单详情,然后进行状态赋值
    //getFormDetail 为自定义的获取表单详情数据的函数
    getFormDetail(this.state.id).then((res)=>{
      if(res.status=="OK"){
        let {startTime, endTime} = res.entity;
        this.setState({
          startTime,
          endTime
        });
      }
    })
  }

Submit:
// 提交的 API 变为修改表单的 API
  handleSubmit(){
    // formatTime 为自定义的时间格式化函数
    let startTime = formatTime(this.state.startTime);
    let endTime = formatTime(this.state.endTime);
    let id = this.state.id;
    // 提交数据
    editForm(
      {
        id,
        startTime,
        endTime
      }
    ).then(function() {
      // do sth
    })
  }
复制代码

完整代码如下:

import React, { Component } from 'react';
import { DatePicker } from 'antd';
import 'antd/dist/antd.css';
import moment from 'moment';

class Form extends Component{
  constructor(props){
    super(props)
    this.state = {
      // id 为表单 id
      id: this.props.id,
      // 开始时间
      startTime: moment('12:08:23', 'HH:mm:ss'),
      // 结束时间
      endTime: moment('13:08:23', 'HH:mm:ss'),
    }
    this.onStartChange.bind(this);
    this.onEndChange.bind(this);
  }
  componentDidMount(){
    // 获取表单详情,然后进行状态赋值
    //getFormDetail 为自定义的获取表单详情数据的函数
    getFormDetail(this.state.id).then((res)=>{
      if(res.status=="OK"){
        let {startTime, endTime} = res.entity;
        this.setState({
          startTime,
          endTime
        });
      }
    })
  }
  onStartChange(value){
    // displayTimestamp 自定义的将 moment 对象生成时间戳的方法
    if(this.state.endTime){
      let endValue = displayTimestamp(this.state.endTime);
      let startValue = displayTimestamp(value);
      // 比较两个时间的时间戳
      if(startValue > endValue){
        return ;
      }
    }
    else {
      this.setState({
        startTime: value
      })
    }
  }
  onEndChange(value){
    // displayTimestamp 自定义的将 moment 对象生成时间戳的方法
    if(this.state.startTime){
      let endValue = displayTimestamp(value);
      let startValue = displayTimestamp(this.state.startTime);
      if(startValue > endValue){
        return ;
      }
    }
    else {
      this.setState({
        endTime: value
      })
    }
  }

  handleSubmit(){
    // formatTime 为自定义的时间格式化函数
    let startTime = formatTime(this.state.startTime);
    let endTime = formatTime(this.state.endTime);
    let id = this.state.id;
    // 提交数据
    editForm(
      {
        id,
        startTime,
        endTime
      }
    ).then(function() {
      // do sth
    })
  }

  render(){
    return(
      <div>
        <div>
          <DatePicker
            showTime
            format="HH:mm:ss"
            value={this.state.startTime}
            onChange={this.onStartChange}
          />
          <DatePicker
            showTime
            format="HH:mm:ss"
            value={this.state.endTime}
            onChange={this.onEndChange}
          />
        </div>
        <div class="submit" onClick={ handleSubmit }/>
      </div>
    )
  }
}
复制代码

使用 redux 重构

小锅想了一下,如果使用 redux(为了更便捷,这里的 redux 指的都是引入了 redux-react 之后的 redux) ,数据的变化就有一些区别,因为组件已经订阅了全局的store,并且做了组件属性和store的映射,所以,组件的数据变化为:初始化的时候(constructor),属性变化的时候(componentWillReceiveProps)。
小锅对自己之前的代码进行了重构。

conponentWillReceiveProps:
// 因为做了 formInfo 和 store 的映射,所以这里根据状态的变化直接赋值
  componentWillReceiveProps(nextProps){
    if(nextProps.formInfo && nextProps.formInfo.status && 
nextProps.formInfo.status=="OK"){
      let {startTime, endTime} = nextProps.formInfo.entity;
      this.setState({
        startTime, endTime
      })
    }
  }
复制代码

完整的代码如下:

import React, { Component } from 'react';
import { DatePicker } from 'antd';
import 'antd/dist/antd.css';
import moment from 'moment';
import { connect } from 'react-redux';
import { updateForm, getFormDetail } from "actions/form";

class Form extends Component{
  constructor(props){
    super(props)
    this.state = {
      // id 为表单 id
      id: this.props.id,
      // 开始时间
      startTime: moment('12:08:23', 'HH:mm:ss'),
      // 结束时间
      endTime: moment('13:08:23', 'HH:mm:ss'),
    }
    this.onStartChange.bind(this);
    this.onEndChange.bind(this);
  }
  componentDidMount(){
    //getFormDetail 为自定义的获取表单详情数据的函数
    getFormDetail(this.state.id);
  }

  componentWillReceiveProps(nextProps){
    if(nextProps.formInfo && nextProps.formInfo.status && 
nextProps.formInfo.status=="OK"){
      let {startTime, endTime} = nextProps.formInfo.entity;
      this.setState({
        startTime, endTime
      })
    }
  }
  onStartChange(value){
    // displayTimestamp 自定义的将 moment 对象生成时间戳的方法
    if(this.state.endTime){
      let endValue = displayTimestamp(this.state.endTime);
      let startValue = displayTimestamp(value);
      // 比较两个时间的时间戳
      if(startValue > endValue){
        return ;
      }
    }
    else {
      this.setState({
        startTime: value
      })
    }
  }
  onEndChange(value){
    // displayTimestamp 自定义的将 moment 对象生成时间戳的方法
    if(this.state.startTime){
      let endValue = displayTimestamp(value);
      let startValue = displayTimestamp(this.state.startTime);
      if(startValue > endValue){
        return ;
      }
    }
    else {
      this.setState({
        endTime: value
      })
    }
  }

  handleSubmit(){
    // formatTime 为自定义的时间格式化函数
    let startTime = formatTime(this.state.startTime);
    let endTime = formatTime(this.state.endTime);
    let id = this.state.id;
    // 提交数据
    editForm(
      {
        id,
        startTime,
        endTime
      }
    ).then(function() {
      // do sth
    })
  }

  render(){
    return(
      <div>
        <div>
          <DatePicker
            showTime
            format="HH:mm:ss"
            value={this.state.startTime}
            onChange={this.onStartChange}
          />
          <DatePicker
            showTime
            format="HH:mm:ss"
            value={this.state.endTime}
            onChange={this.onEndChange}
          />
        </div>
        <div class="submit" onClick={ handleSubmit }/>
      </div>
    )
  }
}
function mapStateToProps(state,ownProps) {
  return {
    formInfo: state.formInfo
  };
}
export default connect()(Form)
复制代码

总结和思考

非受控组件更方便快捷,代码量小,但是控制能力比较弱。受控组件的控制能力强,但是代码量会比较多,在开发中应该权衡需求,进度进行相应的选择。

参考资料

doc.react-china.org/docs/forms.…
doc.react-china.org/docs/uncont…
goshakkk.name/controlled-…
redux.js.org/
github.com/reduxjs/rea…


本文首发于公众号“前端之心”