原文: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
,则要设置 value
和 onChange()
属性。在这种情况下,一旦 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 关注我们哦