原文鏈接:React: hybrid controlled components in action
FBI WARNING: 對於提倡無狀態設計的React來說這可能是一種反模式。
眾所周知,有很多web組件可以通過用戶交互改變它的狀態,如<input>
,<select>
,或者我們常用的一些在線富文本編輯器。這些組件在日常開發中不是很起眼 - 我們可以通過在其中鍵入內容或設置value
屬性來輕松修改它的值。但是,由於React是單向數據綁定的,在React中使用這些組件不是很好控制它的狀態:
1.一個維護自身
State
的input
組件不能從外部修改它的狀態;
2.一個input
組件的值如果由外部props
傳入,則其值受外部控制;
基於上述兩個特點,React提出了受控組件和非受控組件的概念。
受控組件
一個受控的
input
組件接受一個value
屬性,渲染一個<input>
元素,其值反應value
屬性的值。受控組件不保存其自身的內部狀態; 該組件純粹根據
props
呈現內容。
也就是說,如果我們有一個通過props
設置value
的input
組件,它將持續顯示props.value
,即使你通過鍵盤輸入字符。換句話說,你的組件是只讀的。
很多流行的組件都是以這種方式運行。如果我們將類似value
這樣的屬性從這些組件中移除,你好發現它會變成一個“死亡的組件”, - 誰會愛死一個死人呢?
在使用受控組件時,您必須始終傳遞一個value
屬性,同時注冊一個onChange
處理程序,才能以使它們處於活動狀態,如此一來,它的上層組件會變的復雜和混亂。
非受控組件
一個不帶
value
屬性的input
組件是非受控組件。用戶的任何輸入都將立即被反應在渲染元素上。不受控制的組件保持其自身的內部狀態。
這樣的組件運作起來更像原生的組件。可是等等!我們如何像以前那樣通過普通的js操作input.value = xxx
來更改輸入值呢?
遺憾的是,你沒有辦法從外部改變其內部的狀態,因為它是不受控制。
混用受控組件和非受控組件
那么為什么不構建一個既受控又不受控制的組件呢?根據React對(非)受控組件的定義,我們可以得到一些啟示和原則:
原則一
props.value
始終具有比內部state.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>);
}
原則二
組件的任何更改都應同步到內部
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);
});
}
原則三
當組件接收到新的
props
時將props.value
映射到state.value
同步props.value
和state.value
的值是非常關鍵的,它能及時修正內部狀態並保證handleChange
的正確運轉。
componentWillReceiveProps(nextProps) {
const controlledValue = nextProps.value;
if (controlledValue !== undefined &&
controlledValue !== this.state.value
) {
this.setState({
value: controlledValue,
});
}
}
原則四
確保優先的值發生變化才更新組件。
這可以防止組件進行不必要的重新渲染,例如,受控組件在內部state.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;
}
實施方案
綜上所有原則,我們可以創建一個裝飾器如下:
/**
* Optimize hybrid controlled component by add some method into proto
*
* Usage:
* @hybridCtrl
* class App extends React.Component {
* ...
* }
*
* @hybridCtrl('specified_prop_to_assign')
* class App extends React.Component {
* ...
* }
*
* @hybridCtrl('specified_prop_to_assign', '_internal_prop')
* class App extends React.Component {
* ...
* }
*/
import shallowCompare from 'react-addons-shallow-compare';
const noop = () => {};
const optimizer = (Component, key = 'value', internalKey = `_${key}`) => {
// need `this`
function shallowCompareWithExcept(nextProps, nextState) {
const props = {
...nextProps,
[key]: this.props[key], // patched with same value
};
const state = {
...nextState,
[internalKey]: this.state[internalKey],
};
return shallowCompare(this, props, state);
}
const {
shouldComponentUpdate = shallowCompareWithExcept,
componentWillReceiveProps = noop,
} = Component.prototype;
Object.defineProperty(Component.prototype, 'displayValue', {
get: function getDisplayValue() {
// prefer to use `props[key]`
return this.props[key] !== undefined ?
this.props[key] : this.state[internalKey];
},
});
// assign new props to state
Object.defineProperty(Component.prototype, 'componentWillReceiveProps', {
configurable: false,
enumerable: false,
writable: true,
value: function componentWillReceivePropsWrapped(nextProps) {
const controlledValue = nextProps[key];
if (controlledValue !== undefined &&
controlledValue !== this.state[internalKey]
) {
this.setState({
[internalKey]: this.mapPropToState ?
this.mapPropToState(controlledValue) : controlledValue,
});
}
componentWillReceiveProps.call(this, nextProps);
},
});
// patch shouldComponentUpdate
Object.defineProperty(Component.prototype, 'shouldComponentUpdate', {
configurable: false,
enumerable: false,
writable: true,
value: function shouldComponentUpdateWrapped(nextProps, nextState) {
let result = true;
if (nextProps[key] !== undefined) {
// controlled, use `props[key]`
result &= (nextProps[key] !== this.props[key]);
} else {
// uncontrolled, use `state[internalKey]`
result &= (nextState[internalKey] !== this.state[internalKey]);
}
// logic OR rocks
return result ||
shouldComponentUpdate.call(this, nextProps, nextState);
},
});
return Component;
};
export const hybridCtrl = (keyOrComp, internalKey) => {
if (typeof keyOrComp === 'function') {
return optimizer(keyOrComp);
}
return (Component) => optimizer(Component, keyOrComp, internalKey);
};
這個裝飾器的使用方法如下:
import PropTypes from 'prop-types'
@hybridCtrl
class App extends React.Component {
static propTypes = {
value: 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.value
和state.value
。但props.value
的值在渲染時具有更高的優先級,state.value
反映了組件的真實值。