react 单例组件的实现方式
说到 react 的单例,大家可能一哈子就想到了像 Alert 啊,弹层啊、Confirm 啊之类的。没毛病。单例嘛,就是全局唯一一个实例,不可能同时出现两个嘛。是的。极大部分业务情况下是这样的。所以,怎么实现一个单例组件,是个值得思考的问题。
因地制宜,我们的前提是 react 组件的单例。
使用 react 组件常见的套路是写 jsx,直接声明式地将组件放在它该在地位置,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
render() {
return ( <div className={wrapperClassNames}> <GoBack goBack = {props.goBack} /> <h1 className = 'header-title'>{props.headerTitle}</h1> { rightOptions ? <div className = 'header-right'>{ rightOptions }</div> : null } </div> ); }
|
如果我们想根据某种状态来决定是否显示某个组件,可以三目。这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
render() {
return ( <div className={wrapperClassNames}> { this.state.showModal ? <Modal /> : null } </div> ); }
|
是这样的吧?相信大家也都是这么用的。没没啥大毛病。
没啥毛病,意思是有点小毛病咯?
正如小标题,确实没啥大毛病,却有一些小毛病,我说说我在业务中遇到的问题。
- 动画直接丢失。好理解吧?我这个组件有进场、退场动画,在状态变化、变为不显示时,直接就被干掉了,退场动画写给谁看啊?
- 有多少个 Modal,就要写多少次(除非把 Modal 的数据写在上层组件的 state 里,一并传给 Modal)
跟单例组件有啥关系?
正如前面所说,使用 React,就注定了对组件的使用是声明式的。声明式的组件也意味着满足条件时会直接 render 到页面上(虽然可以用 state 来判断是否显示组件,但这种方式直接导致动画失效,这里排除了这种情况)。一般来说,使用单例组件可以采用调用的形式,这里引用一个同事的 Alert 组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| class Alert extends PureComponent { constructor(props) { super(props); this.state = { show: props.show, onConfirm: props.onConfirm, content: props.content }; this.timer = null; that = this; }
componentWillUnmount() { if (container) { document.body.removeChild(container); } }
render() { const { show, content, onConfirm } = this.state; const actions = [{ text: '确定', callback: () => { onConfirm(); this.setState({ show: false }); } }]; return ( <Modal title ='提示' footer onHide = {() => false} actions = {actions} show = {show} > { content } </Modal> ); } }
Alert.propTypes = { show: PropTypes.bool, onConfirm: PropTypes.func, content: PropTypes.string };
Alert.defaultProps = { show: false, onConfirm: () => true, content: '' };
if (!isNodeEnv()) { container = document.createElement('div'); document.body.appendChild(container); ReactDom.render(<Alert />, container); }
export default { alert(config) { that.setState(Object.assign({}, config, { show: true })); } };
|
如上,对外暴露的不再是一个组件,而是包含 alert 方法的对象。通过手动调用 alert (config) 的方式,实现了单例组件。这种方式也非常常见。但这种方式有一个弊端,是什么呢?思考下。
使用 API 调用的方式实现单例组件
是组件吧?那我们肯定要传一些参数对吧?(不要把 Alert 这个单例组件那来当话题背景,你可以想象一个模态框弹层组件,除了外面的掩层,里面的内容是不是得完全自己去写呀?)调用 api 的话,必须每次都把配置对象传入,可能是很大一个对象,如:
1 2 3 4 5 6 7
| globalLayer.show({ title: '测试', onHide: function() {}, onClick: function() {}, content: (<Component1> <Son/> </Component1>) });
|
这样的配置。而且每次调用这个方法都得传一个大对象过去。是不是有点麻烦?
使用声明式组件实现单例
什么是声明组件?就是:
1 2 3 4 5 6 7 8 9 10 11 12 13
| render() { <GlobalLayer show = {this.state.showLayer1}> </GlobalLayer>
<GlobalLayer show = {this.state.showLayer2}> </GlobalLayer> <GlobalLayer show = {this.state.showLayer3}> </GlobalLayer> }
|
类似这种的 “直接 render“。虽然看上去被直接 render 了,看上去应该有 3 个被塞入 DOM 了。但巧妙的就是 GlobalLayer 是一个高阶函数,它管理了自己的 state 中显示逻辑 —- 用这个 state 来控制 children 是否显示。这里贴一下我的高阶函数的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| import React, { PureComponent } from 'react'; import LazyRender from '../LazyRender'; export default function (MyComponent) {
class Wrapper extends PureComponent {
static displayName = 'SelfDeleteWrapper';
state = { showComponent: false };
static getDerivedStateFromProps(props, state) { if (props.show && !state.showComponent) { return { showComponent: true }; }
return null; } deleteComponent = () => { console.log('1'); this.setState({ showComponent: false }); }
LazyComponent = LazyRender(MyComponent);
render() {
const { showComponent } = this.state;
const { deleteComponent, LazyComponent } = this;
return ( showComponent ? <LazyComponent {...this.props} __onDelete={deleteComponent} /> : null ); } }
return Wrapper; }
|
关键点还是在于__onDelete 函数。在 MyComponent 触发了 onHide 函数或者被上层组件设置为 show: false 时会触发__onDelete,使 LazyComponent 这个组件被 react 干掉。当然,触发__onDelete 是在执行完 MyComponent 的退场动画后才触发的。这样保证了全局单一的实例。