React專案中Uncontrolled Component的運用

站酷前端小組發表於2018-08-20

uncontrolled是React中一個很重要概念,起源於(不知該概念是否更早在其它領域出現過)React對一些form元素(input, textarea等)的封裝,官方文件給出一些描述:

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.

實際上,uncontrolled思想的運用已經遠遠超出了form元素的範疇,合理的使用uncontrolled component可以很大程度的簡化程式碼,提高專案的可維護性。本文將結合幾個常用的例子,總結個人在專案實踐中對uncontrolled思想的運用。如有錯誤,歡迎指出。

Uncontrolled Component在可維護性上的優勢。

“高內聚低耦合”是模組設計中很重要的原則。對於一些純UI元件,uncontrolled模式將狀態封裝於元件內部,減少元件通訊,非常符合這一原則。著名的開源專案React-Draggable為我們提供了很好的示例。

可拖拽元件的uncontrolled實現:

import React from 'react'
import Draggable from 'react-draggable'

class App extends React.Component {
  render() {
    return (
      <Draggable>
        <div>Hello world</div>
      </Draggable>
    );
  }
}
複製程式碼

可拖拽元件的controlled實現:

import React from 'react'
import {DraggableCore} from 'react-draggable'

class App extends React.Component {
   state = {
    position: {x: 0, y: 0}
  }

  handleChange = (ev, v) => {
    const {x, y} = this.state.position
    const position = {
      x: x + v.deltaX,
      y: y + v.deltaY,
    }

    this.setState({position})
  }

  render() {

    const {x, y} = this.state.position
    return (
      <DraggableCore
        onDrag={this.handleChange}
        position={this.state.position}
      >
        <div style={{transform: `translate(${x}px, ${y}px)`}}>
          Hello world
        </div>
      </DraggableCore>
    );
  }
}

複製程式碼

比較以上兩個示例,uncontrolled component將拖拽的實現邏輯、元件位置對應的state等全部封裝在元件內部。作為使用者,我們絲毫不用關心其的運作原理,即使出現BUG,定位問題的範圍也可以鎖定在元件內部,這對提高專案的可維護性是非常有幫助的。

Mixed Component元件的具體實現

上文提到的React-Draggable功能實現相對複雜,依據controlled和uncontrolled分成了兩個元件,更多的時候,往往是一個元件承載了兩種呼叫方式。(Mixed Component) 例如Ant.Design存在有許多例子:

  • Pagination元件中有currentdefaultCurrent
  • Switch元件中的checkeddefaultChecked
  • Slider元件中的valuedefaultValue

把兩種模式集中在一個元件中,如何更好的組織程式碼呢?以Switch為例:

class Switch extends Component {
  constructor(props) {
    super(props);

    let checked = false;

    // 'checked' in props ? controlled : uncontrolled
    if ('checked' in props) {
      checked = !!props.checked;
    } else {
      checked = !!props.defaultChecked;
    }
    this.state = { checked };
  }

  componentWillReceiveProps(nextProps) {
    // 如果controlled模式,同步props,以此模擬直接使用this.props.checked的效果
    if ('checked' in nextProps) {
      this.setState({
        checked: !!nextProps.checked,
      });
    }
  }

  handleChange(checked) {
    // controlled: 僅觸發props.onChange
    // uncontrolled: 內部改變checked狀態
    if (!('checked' in this.props)) {
      this.setState({checked})
    }

    this.props.onChange(checked)
  }

  render() {
    return (
      // 根據this.state.checked 實現具體UI即可
    )
  }
}

複製程式碼

Uncontrolled思想在類Modal元件的擴充套件

在一般React的專案中,我們通常會使用如下的方式呼叫Modal元件:

class App extends React.Component {
  state = { visible: false }

  handleShowModal = () => {
    this.setState({ visible: true })
  }

  handleHideModal = () => {
    this.setState({ visible: false })
  }

  render() {
    return (
      <div>
        <button onClick={this.handleShowModal}>Open</button>
        <Modal
          visible={this.state.visible}
          onCancel={this.handleHideModal}
        >
          <p>Some contents...</p>
          <p>Some contents...</p>
        </Modal>
      </div>
    )
  }
}
複製程式碼

根據React渲染公式UI=F(state, props),這麼做並沒有什麼問題。但是如果在某個元件中大量(不用大量,三個以上就深感痛苦)的使用到類Modal元件,我們就不得不定義大量的visible state和click handle function分別控制每個Modal的展開與關閉。最具代表性的莫過於自定義的Alert和Confirm元件,如果每次與使用者互動都必須通過state控制,就顯得過於繁瑣,莫名地增加專案複雜度。 因此,我們可以將uncontrolled的思想融匯於此,嘗試將元件的關閉封裝於元件內部,簡化大量冗餘的程式碼。以Alert元件為例:

// Alert UI元件,將destroy繫結到需要觸發的地方
class Alert extends React.Component {
  static propTypes = {
    btnText: PropTypes.string,
    destroy: PropTypes.func.isRequired,
  }

   static defaultProps = {
    btnText: '確定',
  }

  render() {
    return (
      <div className="modal-mask">
        <div className="modal-alert">
          {this.props.content}
          <button
            className="modal-alert-btn"
            onClick={this.props.destroy}
          >
            {this.props.btnText}
          </button>
        </div>
      </div>
    )
  }
}

// 用於渲染的中間函式,建立一個destroy傳遞給Alert元件
function uncontrolledProtal (config) {
  const $div = document.createElement('div')
  document.body.appendChild($div)

  function destroy() {
    const unmountResult = ReactDOM.unmountComponentAtNode($div)
    if (unmountResult && $div.parentNode) {
      $div.parentNode.removeChild($div)
    }
  }

  ReactDOM.render(<Alert destroy={destroy} {...config} />, $div)

  return { destroy, config }
}

/**
 * 考慮到API語法的優雅,我們常常會把類似功能的元件統一export。例如:
 *    https://ant.design/components/modal/
 *    Modal.alert
 *    Modal.confirm
 *
 *    https://ant.design/components/message/
 *    message.success
 *    message.error
 *    message.info
 */
export default class Modal extends React.Component {
  // ...
}

Modal.alert = function (config) {
  return uncontrolledProtal(config)
}

複製程式碼

以上我們完成了一個uncontrolled模式的Alert,現在呼叫起來就會很方便,不再需要定義state去控制show/hide了。線上預覽

import Modal from 'Modal'

class App extends React.Component {
  handleShowModal = () => {
    Modal.alert({
      content: <p>Some contents...</p>
    })
  }

  render() {
    return (
      <div>
        <button onClick={this.handleShowModal}>Open</button>
      </div>
    )
  }
}
複製程式碼

結語

uncontrolled component在程式碼簡化,可維護性上都有一定的優勢,但是也應該把握好應用場景:“確實不關心元件內部的狀態”。其實在足夠複雜的專案中,多數場景還是需要對所有元件狀態有完全把控的能力(如:撤銷功能)。學習一樣東西,並不一定是隨處可用,重要的是在最契合的場景,應該下意識的想起它。

相關文章