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

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

你可曾踟蹰过该建立受控组件仍是非受控组件呢?java

一些背景

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

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

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

React 中的 Inputs

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

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

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

通常模式

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

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

要建立一个受控组件,首先定义好想要控制的属性 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 关注咱们哦

相关文章
相关标签/搜索