React:受控組件與非受控組件混用實戰 - 譯文


原文鏈接:React: hybrid controlled components in action

FBI WARNING: 對於提倡無狀態設計的React來說這可能是一種反模式。

眾所周知,有很多web組件可以通過用戶交互改變它的狀態,如<input><select>,或者我們常用的一些在線富文本編輯器。這些組件在日常開發中不是很起眼 - 我們可以通過在其中鍵入內容或設置value屬性來輕松修改它的值。但是,由於React是單向數據綁定的,在React中使用這些組件不是很好控制它的狀態:

1.一個維護自身Stateinput組件不能從外部修改它的狀態;
2.一個input組件的值如果由外部props傳入,則其值受外部控制;

基於上述兩個特點,React提出了受控組件非受控組件的概念。

受控組件

一個受控的input組件接受一個value屬性,渲染一個<input>元素,其值反應value屬性的值。

受控組件不保存其自身的內部狀態; 該組件純粹根據props呈現內容。

也就是說,如果我們有一個通過props設置valueinput組件,它將持續顯示props.value,即使你通過鍵盤輸入字符。換句話說,你的組件是只讀的。

很多流行的組件都是以這種方式運行。如果我們將類似value這樣的屬性從這些組件中移除,你好發現它會變成一個“死亡的組件”, - 誰會愛死一個死人呢?

在使用受控組件時,您必須始終傳遞一個value屬性,同時注冊一個onChange處理程序,才能以使它們處於活動狀態,如此一來,它的上層組件會變的復雜和混亂。

非受控組件

一個不帶value屬性的input組件是非受控組件。用戶的任何輸入都將立即被反應在渲染元素上。

不受控制的組件保持其自身的內部狀態。

這樣的組件運作起來更像原生的組件。可是等等!我們如何像以前那樣通過普通的js操作input.value = xxx來更改輸入值呢?

遺憾的是,你沒有辦法從外部改變其內部的狀態,因為它是不受控制。

混用受控組件和非受控組件

那么為什么不構建一個既受控又不受控制的組件呢?根據React對(非)受控組件的定義,我們可以得到一些啟示和原則:

原則一

props.value始終具有比內部state.value更高的優先級。

當設置了props.value,我們應該始終使用其值代替state.value渲染組件,所以我們可以定義一個displayValue getter屬性:

get displayValue() {  
  return this.props[key] !== undefined ?
    this.props[key] : this.state[internalKey];
}

然后在render功能:

render() {  
  return (<div>{this.displayValue}</div>);
}

原則二

組件的任何更改都應同步到內部state.value,然后通過props.onChange請求更新上層組件的狀態。

將值同步到state.value可以確保組件在不受控制時能夠呈現最新值。請求外部更新告訴上層組件執行更改props.value,因此受控組件也可以呈現正確的值。

handleChange(newVal) {  
  if (newVal === this.state.value) return;

  this.setState({
    value: newVal,
  }, () => {
    this.props.onChange && this.props.onChange(newVal);
  });
}

原則三

當組件接收到新的props時將props.value映射到state.value

同步props.valuestate.value的值是非常關鍵的,它能及時修正內部狀態並保證handleChange的正確運轉。

componentWillReceiveProps(nextProps) {  
  const controlledValue = nextProps.value;

  if (controlledValue !== undefined &&
      controlledValue !== this.state.value
  ) {
    this.setState({
      value: controlledValue,
    });
  }
}

原則四

確保優先的值發生變化才更新組件。

這可以防止組件進行不必要的重新渲染,例如,受控組件在內部state.value更改時不應觸發重新渲染。

shouldComponentUpdate(nextProps, nextState) {  
  if (nextProps.value !== undefined) {
    // controlled, use `props.value`
    return nextProps.value !== this.props.value;
  }

  // uncontrolled, use `state.value`
  return nextState.value !== this.state.value;
}

實施方案

綜上所有原則,我們可以創建一個裝飾器如下:

/**
 * Optimize hybrid controlled component by add some method into proto
 *
 * Usage:
 *   @hybridCtrl
 *   class App extends React.Component {
 *     ...
 *   }
 *
 *   @hybridCtrl('specified_prop_to_assign')
 *   class App extends React.Component {
 *     ...
 *   }
 *
 *  @hybridCtrl('specified_prop_to_assign', '_internal_prop')
 *   class App extends React.Component {
 *     ...
 *   }
 */

import shallowCompare from 'react-addons-shallow-compare';

const noop = () => {};

const optimizer = (Component, key = 'value', internalKey = `_${key}`) => {
  // need `this`
  function shallowCompareWithExcept(nextProps, nextState) {
    const props = {
      ...nextProps,
      [key]: this.props[key], // patched with same value
    };

    const state = {
      ...nextState,
      [internalKey]: this.state[internalKey],
    };

    return shallowCompare(this, props, state);
  }

  const {
    shouldComponentUpdate = shallowCompareWithExcept,
    componentWillReceiveProps = noop,
  } = Component.prototype;

  Object.defineProperty(Component.prototype, 'displayValue', {
    get: function getDisplayValue() {
      // prefer to use `props[key]`
      return this.props[key] !== undefined ?
        this.props[key] : this.state[internalKey];
    },
  });

  // assign new props to state
  Object.defineProperty(Component.prototype, 'componentWillReceiveProps', {
    configurable: false,
    enumerable: false,
    writable: true,
    value: function componentWillReceivePropsWrapped(nextProps) {
      const controlledValue = nextProps[key];
      if (controlledValue !== undefined &&
          controlledValue !== this.state[internalKey]
      ) {
        this.setState({
          [internalKey]: this.mapPropToState ?
            this.mapPropToState(controlledValue) : controlledValue,
        });
      }

      componentWillReceiveProps.call(this, nextProps);
    },
  });

  // patch shouldComponentUpdate
  Object.defineProperty(Component.prototype, 'shouldComponentUpdate', {
    configurable: false,
    enumerable: false,
    writable: true,
    value: function shouldComponentUpdateWrapped(nextProps, nextState) {
      let result = true;

      if (nextProps[key] !== undefined) {
        // controlled, use `props[key]`
        result &= (nextProps[key] !== this.props[key]);
      } else {
        // uncontrolled, use `state[internalKey]`
        result &= (nextState[internalKey] !== this.state[internalKey]);
      }

      // logic OR rocks
      return result ||
        shouldComponentUpdate.call(this, nextProps, nextState);
    },
  });

  return Component;
};

export const hybridCtrl = (keyOrComp, internalKey) => {
  if (typeof keyOrComp === 'function') {
    return optimizer(keyOrComp);
  }

  return (Component) => optimizer(Component, keyOrComp, internalKey);
};

這個裝飾器的使用方法如下:

import PropTypes from 'prop-types'
@hybridCtrl
class App extends React.Component {  
  static propTypes = {
    value: PropTypes.any,
  }

  state = {
    _value: '',
  }

  mapPropToState(controlledValue) {
    // your can do some transformations from `props.value` to `state._value`
  }

  handleChange(newVal) {
    // it's your duty to handle change events and dispatch `props.onChange`
  }
}

總結

1.我們為什么需要混合受控組件和非受控組件?(什么場合需要使用雜交組件?)

我們需要創建同時受控和非受控制的組件,就像原生組件一樣。

2.混合的主要思想是什么?

同時維護props.valuestate.value。但props.value的值在渲染時具有更高的優先級,state.value反映了組件的真實值。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM