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在代码简化,可维护性上都有一定的优势,但是也应该把握好应用场景:“确实不关心组件内部的状态”。其实在足够复杂的项目中,多数场景还是需要对所有组件状态有完全把控的能力(如:撤销功能)。学习一样东西,并不一定是随处可用,重要的是在最契合的场景,应该下意识的想起它。