React项目中Uncontrolled Component的运用

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在代码简化,可维护性上都有一定的优势,但是也应该把握好应用场景:“确实不关心组件内部的状态”。其实在足够复杂的项目中,多数场景还是需要对所有组件状态有完全把控的能力(如:撤销功能)。学习一样东西,并不一定是随处可用,重要的是在最契合的场景,应该下意识的想起它。

猜你喜欢

转载自juejin.im/post/5b7a2d4d51882542ed141c7c