在實際業務中如何靈活運用受控元件與非受控元件

端端君發表於2018-06-01

概況

在 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…


本文首發於公眾號“前端之心”


相關文章