由一行文本輸入框引起的思考

  文章是關於React組件之表單單行文本輸入框的一些思考。可能你們第一反應都是,不就是一行<input/>嘛,沒什麼特別的吧?若是說到輸入框的的話,可能圈子裏上大多數封裝好的ReactUI組件庫中使用的方式無非都是在組件中經過 Props傳值給Input組件,而後在Input組件的onChange回掉中改變當前組件的State,從而達到改變Input組件Props的目的。html

  這裏引起一個思考,若是父組件相對複雜,當其中一個Input組件的值被改變時,會更新State觸發render和其餘全部可能與State相關的子組件(這些字組件的Prop都是經過父組件的State來傳遞的)的更新,簡單的想象一下,咱們在輸入框中每敲一下鍵盤都會觸發一系列的代碼邏輯,這可能會是多恐怖的一件事。node

經過Props傳值的必要性

  Input輸入框必然是有value這一屬性的,若是這個屬性是做爲State或者Props傳遞的,要改變這一屬性,也是必然經過改變Props或者State來實現,而改變的途徑有且只有一個,就是在Input的onChange裏面進行監聽,正如官方所給的示例代碼同樣Forms-React:react

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

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

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

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}複製代碼

  若是咱們想要把這個Input輸入框封裝成爲一個單獨的組件,按照咱們正常的處理邏輯,這個組件的value必定是經過Props傳遞過來的,當咱們把這個Props做爲Input的屬性值以後,想要改變的話也就只能經過父組件改變傳遞的Props來實現了,不可避免的產生了一些改變值和傳遞值的代碼邏輯。git

一些優秀UI庫的常規處理

  瞭解了Input輸入框的傳值機制以後,咱們來看看圈子裏一些優秀組件庫的處理方式是否和咱們預想的一致,是否也有這些不可變的代碼邏輯問題。github

React-Bootstrap

  React-Boostrap算是Bootstrap的React版,其權威性也是可見一斑。官方的示例代碼也是徹底和咱們預期的一致,經過onChang回掉來改變父組件的State,以後再把State做爲Props傳遞給子組件,有興趣可自行查閱。而咱們打開源碼來看,關鍵部分以下:bootstrap

class FormControl extends React.Component {
  render() {
      ...
    const {
      componentClass: Component,
      type,
      id = controlId,
      inputRef,
      className,
      bsSize,
      ...props
    } = this.props;

    const [bsProps, elementProps] = splitBsProps(props);
    ...
    return (
      <Component {...elementProps} type={type} id={id} ref={inputRef} className={classNames(className, classes)} /> ); } }複製代碼

  能夠解讀成就是一個標籤的封裝,沒有本身的State,其值徹底依賴於Props的改變,這也和咱們預期的結果吻合,而溫和的結果就是咱們最開始拋出的一些疑問,看來在這裏也沒獲得解決。antd

ant-design

  對比一下國人寫的ant-design,直接看官方示例代碼,當看到這裏的時候是否又些小激動呢,代碼以下:框架

import { Input } from 'antd';

ReactDOM.render(<Input placeholder="Basic usage" />, mountNode);複製代碼

  彷佛擺脫了在回掉的onChange中改變State來傳遞Props的方式,代碼簡潔明瞭。打開源碼來看看,ant-design,截取關鍵部分代碼:ide

renderInput() {
    const { value, className } = this.props;
    const otherProps = omit(this.props, [
      'prefixCls',
      'onPressEnter',
      'addonBefore',
      'addonAfter',
      'prefix',
      'suffix',
    ]);

    if ('value' in this.props) {
      otherProps.value = fixControlledValue(value);
      // Input elements must be either controlled or uncontrolled,
      // specify either the value prop, or the defaultValue prop, but not both.
      delete otherProps.defaultValue;
    }
    return this.renderLabeledIcon(
      <input {...otherProps} className={classNames(this.getInputClassName(), className)} onKeyDown={this.handleKeyDown} ref="input" />, ); }複製代碼

  能夠很明顯的看到,整個組件也並無State,全部的狀態也都是經過Props來進行管理的,並無看出其奇特之處在哪兒。若是咱們在官方示例中加入一個屬性,value="xxxx",如此一來,想要改變這個值也是和以前的處理一模一樣。函數

react-ui

  也是國人寫的一個框架,相對來講名氣較小。直接打開其源碼來看,react-ui,關鍵代碼以下:

constructor (props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
  }

  handleChange (event) {
    const { type } = this.props

    let value = event.target.value
    if (value && (type === 'integer' || type === 'number')) {
      if (!Regs[type].test(value)) return
    }
    this.props.onChange(value)
  }複製代碼

  很常規的一種處理,和上面的處理方式惟一不一樣在於語法上面的一些差別,可能這也並不是咱們想要看到的。

  是否真的有那麼一種處理機制和咱們預想不同,而且可以解決咱們的疑問呢?

MUI

  直接打開源碼來看,MUI,關鍵部分以下:

constructor(props) {
    super(props);
    let value = props.value;
    let innerValue = value || props.defaultValue;

    if (innerValue === undefined) innerValue = '';

    this.state = {
      innerValue: innerValue,
      isTouched: false,
      isPristine: true
    };
    ...
  }
  componentWillReceiveProps(nextProps) {
    if ('value' in nextProps) this.setState({ innerValue: nextProps.value });
  }
  onChange(ev) {
    this.setState({
      innerValue: ev.target.value,
      isPristine: false
    });
    ...
  }複製代碼

  能夠看到,這個UI的處理方式和以前所見就都有所不一樣了,不一樣之處在於,把傳入進來的Props.value又作了額外的處理,塞到State裏面去了,這樣改變input value的方式就能夠只改變當前這個組件的state來進行了,而不須要再次經過改變回掉到父組件改變State,從新傳入Props的方式來進行。看到這裏是否是有一絲的欣慰呢?可是冷靜一想,好像是把問題搞複雜了:

  • 爲何須要經過計算傳入Props來看成State呢,既然能夠經過Props計算獲得,拿這個屬性應該不是一個State;
  • componentWillReceiveProps中又要處理判斷一些邏輯,並且這個邏輯頗有可能要與本身的業務相關,從而來決定是否須要改變State,這些邏輯並不是是咱們想要看到的。

metarial-design

  所謂扁平化至極的風格,看看源碼裏面究竟怎麼處理這個問題呢,metarial-design:

handleRefInput = node => {
    this.input = node;
    if (this.props.inputRef) {
      this.props.inputRef(node);
    }
  };

let inputProps = {
      ref: this.handleRefInput,
      ...inputPropsProp,
    };

    return (
      <div onBlur={this.handleBlur} onFocus={this.handleFocus} className={className} {...other}> <InputComponent {...inputProps} /> </div> );複製代碼

  其中有一處關鍵處理和以前看到的都不同,這個ref屬性,直接賦值this.input=input(固然它還有額外的函數處理,這裏並不影響),這行代碼究竟有什麼神奇的效果嘛,應該只是單純的直接從DOM裏面取值或者進行一些直接修改DOM的操做吧?。最後看看官方有沒有相關的解釋。

React官網

  仔細查閱,找到了一篇相關的文章說明Uncontroled Components。這篇文章詳細的介紹咱們的疑惑(看來這些疑問並不是是想固然的,實實在在有這個問題)。

In most cases, we recommend using controlled components to implement forms. In a controlled component, form data is handled by a React component. The alternative is uncontrolled components, where form data is handled by the DOM itself.

  看到這裏應該就豁然開朗了,因此所謂正確的姿式:

render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={(input) => this.input = input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }複製代碼

  看了以後,就能理解material-design和ant-design的寫法了,若是想要默認值,正如官方所說:

In the React rendering lifecycle, the value attribute on form elements will override the value in the DOM. With an uncontrolled component, you often want React to specify the initial value, but leave subsequent updates uncontrolled. To handle this case, you can specify a defaultValue attribute instead of value.

render() {
  return (
    <form onSubmit={this.handleSubmit}>
      <label>
        Name:
        <input
          defaultValue="Bob"
          type="text"
          ref={(input) => this.input = input} />
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}複製代碼

  來回往復,直接一個defaultValue解決了咱們的疑惑,本來就在官方網站擺着卻看似明白不清楚具體場景,想的太多作的太少。

相關文章
相關標籤/搜索