uncontrolled是React中一個很重要概念,起源於(不知該概念是否更早在其它領域出現過)React對一些form元素(input, textarea等)的封裝,官方文件給出一些描述:
In most cases, we recommend using controlled components to implement forms. In a controlled component, form data is handled by a React component. The alternative is uncontrolled components, where form data is handled by the DOM itself.
實際上,uncontrolled思想的運用已經遠遠超出了form元素的範疇,合理的使用uncontrolled component可以很大程度的簡化程式碼,提高專案的可維護性。本文將結合幾個常用的例子,總結個人在專案實踐中對uncontrolled思想的運用。如有錯誤,歡迎指出。
Uncontrolled Component在可維護性上的優勢。
“高內聚低耦合”是模組設計中很重要的原則。對於一些純UI元件,uncontrolled模式將狀態封裝於元件內部,減少元件通訊,非常符合這一原則。著名的開源專案React-Draggable為我們提供了很好的示例。
可拖拽元件的uncontrolled實現:
import React from 'react'
import Draggable from 'react-draggable'
class App extends React.Component {
render() {
return (
<Draggable>
<div>Hello world</div>
</Draggable>
);
}
}
複製程式碼
可拖拽元件的controlled實現:
import React from 'react'
import {DraggableCore} from 'react-draggable'
class App extends React.Component {
state = {
position: {x: 0, y: 0}
}
handleChange = (ev, v) => {
const {x, y} = this.state.position
const position = {
x: x + v.deltaX,
y: y + v.deltaY,
}
this.setState({position})
}
render() {
const {x, y} = this.state.position
return (
<DraggableCore
onDrag={this.handleChange}
position={this.state.position}
>
<div style={{transform: `translate(${x}px, ${y}px)`}}>
Hello world
</div>
</DraggableCore>
);
}
}
複製程式碼
比較以上兩個示例,uncontrolled component將拖拽的實現邏輯、元件位置對應的state等全部封裝在元件內部。作為使用者,我們絲毫不用關心其的運作原理,即使出現BUG,定位問題的範圍也可以鎖定在元件內部,這對提高專案的可維護性是非常有幫助的。
Mixed Component元件的具體實現
上文提到的React-Draggable功能實現相對複雜,依據controlled和uncontrolled分成了兩個元件,更多的時候,往往是一個元件承載了兩種呼叫方式。(Mixed Component) 例如Ant.Design存在有許多例子:
- Pagination元件中有
current
與defaultCurrent
- Switch元件中的
checked
與defaultChecked
- Slider元件中的
value
與defaultValue
把兩種模式集中在一個元件中,如何更好的組織程式碼呢?以Switch
為例:
class Switch extends Component {
constructor(props) {
super(props);
let checked = false;
// 'checked' in props ? controlled : uncontrolled
if ('checked' in props) {
checked = !!props.checked;
} else {
checked = !!props.defaultChecked;
}
this.state = { checked };
}
componentWillReceiveProps(nextProps) {
// 如果controlled模式,同步props,以此模擬直接使用this.props.checked的效果
if ('checked' in nextProps) {
this.setState({
checked: !!nextProps.checked,
});
}
}
handleChange(checked) {
// controlled: 僅觸發props.onChange
// uncontrolled: 內部改變checked狀態
if (!('checked' in this.props)) {
this.setState({checked})
}
this.props.onChange(checked)
}
render() {
return (
// 根據this.state.checked 實現具體UI即可
)
}
}
複製程式碼
Uncontrolled思想在類Modal元件的擴充套件
在一般React的專案中,我們通常會使用如下的方式呼叫Modal元件:
class App extends React.Component {
state = { visible: false }
handleShowModal = () => {
this.setState({ visible: true })
}
handleHideModal = () => {
this.setState({ visible: false })
}
render() {
return (
<div>
<button onClick={this.handleShowModal}>Open</button>
<Modal
visible={this.state.visible}
onCancel={this.handleHideModal}
>
<p>Some contents...</p>
<p>Some contents...</p>
</Modal>
</div>
)
}
}
複製程式碼
根據React渲染公式UI=F(state, props)
,這麼做並沒有什麼問題。但是如果在某個元件中大量(不用大量,三個以上就深感痛苦)的使用到類Modal元件,我們就不得不定義大量的visible state和click handle function分別控制每個Modal的展開與關閉。最具代表性的莫過於自定義的Alert和Confirm元件,如果每次與使用者互動都必須通過state控制,就顯得過於繁瑣,莫名地增加專案複雜度。
因此,我們可以將uncontrolled的思想融匯於此,嘗試將元件的關閉封裝於元件內部,簡化大量冗餘的程式碼。以Alert元件為例:
// Alert UI元件,將destroy繫結到需要觸發的地方
class Alert extends React.Component {
static propTypes = {
btnText: PropTypes.string,
destroy: PropTypes.func.isRequired,
}
static defaultProps = {
btnText: '確定',
}
render() {
return (
<div className="modal-mask">
<div className="modal-alert">
{this.props.content}
<button
className="modal-alert-btn"
onClick={this.props.destroy}
>
{this.props.btnText}
</button>
</div>
</div>
)
}
}
// 用於渲染的中間函式,建立一個destroy傳遞給Alert元件
function uncontrolledProtal (config) {
const $div = document.createElement('div')
document.body.appendChild($div)
function destroy() {
const unmountResult = ReactDOM.unmountComponentAtNode($div)
if (unmountResult && $div.parentNode) {
$div.parentNode.removeChild($div)
}
}
ReactDOM.render(<Alert destroy={destroy} {...config} />, $div)
return { destroy, config }
}
/**
* 考慮到API語法的優雅,我們常常會把類似功能的元件統一export。例如:
* https://ant.design/components/modal/
* Modal.alert
* Modal.confirm
*
* https://ant.design/components/message/
* message.success
* message.error
* message.info
*/
export default class Modal extends React.Component {
// ...
}
Modal.alert = function (config) {
return uncontrolledProtal(config)
}
複製程式碼
以上我們完成了一個uncontrolled模式的Alert,現在呼叫起來就會很方便,不再需要定義state去控制show/hide了。線上預覽
import Modal from 'Modal'
class App extends React.Component {
handleShowModal = () => {
Modal.alert({
content: <p>Some contents...</p>
})
}
render() {
return (
<div>
<button onClick={this.handleShowModal}>Open</button>
</div>
)
}
}
複製程式碼
結語
uncontrolled component在程式碼簡化,可維護性上都有一定的優勢,但是也應該把握好應用場景:“確實不關心元件內部的狀態”。其實在足夠複雜的專案中,多數場景還是需要對所有元件狀態有完全把控的能力(如:撤銷功能)。學習一樣東西,並不一定是隨處可用,重要的是在最契合的場景,應該下意識的想起它。