[译] React 中的受控组件和非受控组件

1,306 阅读5分钟

原文:https://www.viget.com/articles/controlling-components-react/

你可曾踟蹰过该创建受控组件还是非受控组件呢?

一些背景

如果初涉 React 应用开发,你可能曾嘀咕过:“受控组件和非受控组件是啥?”。那么我建议你额外花点时间先看看官网的文档。

在 React 应用中之所以需要受控组件和非受控组件,起因于<input><textarea><select> 这类特定的 DOM 元素默认在 DOM 层中维持状态(用户输入)。受控组件用来在 React 中也保存该状态,比如同步到渲染输入元素的组件、树结构中的某个父组件,或者一个 flux store 中。

而这种模式可以被扩展至特定的非 DOM 状态相关的用例中。比如,在最近的一个应用中,我需要创建一个可嵌套的 Collapsible 折叠组件,支持两种操作模式:某些情况下需要使其被外界可控(当应用中的其他区域发生用户交互时扩展开),其他时候它能简单的自己管理状态就可以了。

React 中的 Inputs

对于 React 中的 Inputs,是这样工作的:

要创建一个非受控 input,要设置一个 defaultValue 属性。这种情况下 React 组件会使用底层 DOM 节点并借助节点组件本身的 state 管理该 value。撇开实现细节不说,你可以将之想象成调用了组件的 setState() 更新了 state.value 并将之赋值给了 DOM input 元素。

要创建一个受控 input,则要设置 valueonChange() 属性。在这种情况下,一旦 value 属性改变,React 总会将该属性赋值给 input 作为它的值。当用户改变了 input 的值,onChange() 回调会被调用,并必须立即得出一个新的 value 属性值用以发送给 input。因此,如果 onChange() 没被正确的处理,则 input 实际上就成了只读;因为 input 总是靠着 value 属性来渲染其值的,用户也就无法改变 input 的值了。

一般模式

还好,利用这种行为创建组件不算麻烦。关键在于创建一个组件接口,可以在两种可能的属性配置中选择其一。

要创建一个非受控组件,就将想控制的属性定义成 defaultXXX。当一个被定义了 defaultXXX 属性的组件初始化时,将以给定的值开始,并在组件的生命周期中自我管理状态(调用 setState() 以响应用户交互)。这就覆盖了用例1:组件无须被外部控制且状态本地化。

要创建一个受控组件,首先定义好想要控制的属性 xxx。组件以 xxx 属性给定的值和一个用于响应 xxx 改变的回调方法(例如 xxx 是布尔值的话,响应的就是 toggleXXX())被初始化。当用户对该组件做出交互,不同于非受控组件在内部调用了 setState() 的是,组件必须调用 toggleXXX() 回调以请求外部更新相关 state 值。更新过后,容器组件应该以重新渲染并向受控组件发送一个 xxx 值才告一段落。

Collapsible 接口

对于开头提到的 Collapsible 组件, 只处理了一个布尔值属性,所以我选择用 collapsed / defaultCollapsed 和 toggleCollapsed() 作为组件的接口。

当指定一个 defaultCollapsed 属性后,Collapsible 组件将以该属性所声明的状态开始工作,但在其生命周期自我管理状态。点击子按钮会出发一个 setState() 并更新内部组件状态。

而指定一个布尔值的 collapsed 属性以及一个 toggleCollapsed() 回调属性的话,Collapsible 组件也会以 collapsed 属性所声明的值开始工作,但点击的时候,只会去调用 toggleCollapsed() 回调。理想的状况是,由 toggleCollapsed() 更新外层某个组件中的状态,并引发 Collapsible 组件由于得到了新的 collapsed 属性而重新渲染。

实现

有一种非常简单的模式适用于本项工作,其主要思路如下:

当组件被初始化时,将 xxx 传入的值或 xxx 的默认值放入 state 中。在本例中,defaultCollapsed 的默认值是 false。

在渲染阶段,如果定义了 xxx 属性,那么按其行事(受控模式);否则就在 this.state 中使用本地组件的值(非受控模式)。这意味着在 Collapsible 组件的 render 方法中,我是这么决定 collapsed 状态的:

let collapsed = this.props.hasOwnProperty('collapsed') 
    ? this.props.collapsed 
    : this.state.collapsed

利用解构和默认值,也可以让写法更优雅一些:

// 覆盖了受控和非受控两种用例下的状态选择
const {
  collapsed = this.state.collapsed,
  toggleCollapsed
} = this.props

以上代码的意思就是:“给我一个叫做 collapsed 的绑定,从 this.props.collapsed 中取它的值;不过要是那个值没定义,就用 this.state.collapsed 代替”。

封装

对于使你自己的组件同时支持可控/非可控行为这一点上,你应该能明白这是简单而很可能有用的。希望你能清楚的理解为什么需要用这种方式构建组件,并且也知道如何去做。以下正是你所好奇的 Collapsible 组件的完整源码 -- 很简短的。

/**
 * Collapsible 是一个高阶组件,为一个给定的组件提供了可折叠行为。
 * 基于其 `collapsed` 属性,被包装的组件可以决定如何渲染。
 */
import invariant from 'invariant'
import { createElement, Component } from 'react'
import getDisplayName from 'recompose/getDisplayName'
import hoistStatics from 'hoist-non-react-statics'
import PropTypes from 'prop-types'

export default function collapsible(WrappedComponent) {
  invariant(
    typeof WrappedComponent == 'function',
    `You must pass a component to the function returned by ` +
    `collapsible. Instead received ${JSON.stringify(WrappedComponent)}`
  )

  const wrappedComponentName = getDisplayName(WrappedComponent)
  const displayName = `Collapsible(${wrappedComponentName})`

  class Collapsible extends Component {

    static displayName = displayName
    static WrappedComponent = WrappedComponent

    static propTypes = {
      onToggle: PropTypes.func,
      collapsed: PropTypes.bool,
      defaultCollapsed: PropTypes.bool
    }

    static defaultProps = {
      onToggle: () => {},
      collapsed: undefined,
      defaultCollapsed: true
    }

    constructor(props, context) {
      super(props, context)

      this.state = {
        collapsed: props.defaultCollapsed
      }
    }

    render() {
      const {
        collapsed = this.state.collapsed, // 魔术开始了
        defaultCollapsed,
        ...props
      } = this.props

      return createElement(WrappedComponent, {
        ...props,
        collapsed,
        toggleCollapsed: this.toggleCollapsed
      })
    }

    toggleCollapsed = () => {
      this.setState(({ collapsed }) => ({ collapsed: !collapsed }))
      this.props.onToggle()
    }
  }

  return hoistStatics(Collapsible, WrappedComponent)
}
----------------------------------------

长按二维码或搜索 fewelife 关注我们哦