React:创建同时受控与非受控的组件

提示:考虑到react的少状态设计,这里提到的方案可能是反模式。

我们都知道,有许多的web组件可以被用户的交互发生改变,比如:<input><select>,或者是我现在正在使用的富文本编辑器。这些组件在日常的开发中很不显眼,我们可以很轻易的通过输入一些内容或者设置元素的value属性来改变组件的值。但是,因为React是单向数据流绑定的,这些组件可能会变得失控

1.一个维护它自己state里的value值的<Input>组件无法从外部被修改;
2.一个通过props来设置value值的<Input>组件只能通过外部控制来更新。

最终,React提出了两个概念:受控组件非受控组件

受控组件

一个受控的<input>组件都有一个value属性。渲染一个受控的<input>会展示出value属性的值。
一个受控的组件不会维护它自己内部的状态,组件的渲染单纯的依赖于props。

也就是说,如果我们有一个通过props来设置value<input>组件,不管你如何输入,它都只会显示props.value。换句话说,你的组件是只读的。

很神奇的是,一些非常流行的组件也会有这样的行为。可以看下react工具箱集合,如果我们从<Dropdown />组件中移除value属性,那么该组件会变成一个‘死的’(无法被修改的)dropdown – 谁会爱上一个死的家伙?

当我们在处理一个受控组件的时候,你应该始终传一个value属性进去,并且注册一个onChange的辅助方法来让组件变得可变,这样的话,上面组件(译者注:就是包含input或者textarea的外围组件)的state就会变得复杂和混乱。

非受控组件

一个没有value属性的<input>就是一个非受控组件。通过渲染的元素,任意的用户输入都会被立即反映出来。

一个非受控的组件自己维护自己的state

很好,现在我们就可以像原生的元素那样进行操作了。等等,那我如何修改input元素的值呢,就像之前简单的js操作:input.value = xxx那样。

悲剧的是你无法从外面来修改value值,因为它是不受控的!

混合组件

那么,为什么不创建一个既受控不受控的组件呢?我们可以根据react受控(非受控)组件的定义里得到一些启发:

原则1

props.value总是有比内部state.value跟高的优先级。

props.value被设置以后,我们应该总是拿props.value来渲染而不是state.value,因此我们可以定义一个displayValue的getter属性:

get displayValue() {  
      return this.props[key] !== undefined ?
      this.props[key] : this.state[internalKey];
}

然后再render函数中:

render() {  
       return (<div>{this.displayValue}</div>); }

原则2

组件中所有的变化都应该同步到内部的state.value,并通过执行props.onChange来触发更新。

将值同步到state.value可以保证组件即使是非受控的也总能渲染出最新的值。通过请求一个外部的更新会告诉上面的组件基于props.value执行修改,这样受控的组件也能渲染出正确的值。

handleChange(newVal) {
    if (newVal === this.state.value) return;

    this.setState({
        value: newVal,},() => {
        this.props.onChange && this.props.onChange(newVal);
    });
}

原则3

当组件接受新的props的时候,将props.value反映给state.value

为了能修改内部的value以及在handleChange中执行正确的操作,将props.valuestate.value进行同步是非常重要的:

componentWillReceiveProps(nextProps) {
    const controlledValue = nextProps.value;

    if (controlledValue !== undefined &&
        controlledValue !== this.state.value
    ) {
        this.setState({
            value: controlledValue,});
    }
}

原则4

在优先值(译者注:props.value优先于state.value)发生变化后更新组件。

这样能阻止组件发生不必要的二次渲染,比如,一个受控的组件在内部state.value发生变化的时候不应该触发重新渲染(译者注:受控的只有在props.value发生变化的时候才触发修改)。

shouldComponentUpdate(nextProps,nextState) {
    if (nextProps.value !== undefined) {
        // controlled,use `props.value`
        return nextProps.value !== this.props.value;
    }

    // uncontrolled,use `state.value`
    return nextState.value !== this.state.value;
}

解决方案

有了以上的原则后,我们可以像this gist 那样创建一个装饰器。可以这样用:

@hybridCtrl
class App extends React.Component {
    static propTypes = {
        value: React.PropTypes.any,}

    state = {
        _value: '',}

    mapPropToState(controlledValue) {
        // your can do some transformations from `props.value` to `state._value`
    }

    handleChange(newVal) {
        // it's your duty to handle change events and dispatch `props.onChange`
    }
}

结论

1、为什么我们需要混合组件?

我们应该创建同时受控和非受控的组件,就像原生的元素那样。

2、混合组件主要思想是?

同时维护props.valuestate.value的值。props.value在展示上拥有更高的优先级,state.value代表着组件真正的值。

P.S. 在翻译文章后,忍不住写了个input组件的例子试了下,支持以下功能:
1、支持传入默认值;
2、可控:组件外部修改props可改变input组件的真实值及显示值;
3、非可控:输入框中输入值,可同时改变input组件的真实值及显示值。

示例代码地址:https://github.com/abell123456/hybrid-component

相关文章

react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接...
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc ...