• 使用react context实现一个支持组件组合和嵌套的React Tab组件


    纵观react的tab组件中,即使是github上star数多的tab组件,实现原理都非常冗余。

    例如Github上star数超四百星的react-tab,其在render的时候都会动态计算哪个tab是被选中的,哪个该被隐藏:

      getChildren() {
        let index = 0;
        let count = 0;
        const children = this.props.children;
        const state = this.state;
        const tabIds = this.tabIds = this.tabIds || [];
        const panelIds = this.panelIds = this.panelIds || [];
        let diff = this.tabIds.length - this.getTabsCount();
    
        // Add ids if new tabs have been added
        // Don't bother removing ids, just keep them in case they are added again
        // This is more efficient, and keeps the uuid counter under control
        while (diff++ < 0) {
          tabIds.push(uuid());
          panelIds.push(uuid());
        }
    
        // Map children to dynamically setup refs
        return React.Children.map(children, (child) => {
          // null happens when conditionally rendering TabPanel/Tab
          // see https://github.com/rackt/react-tabs/issues/37
          if (child === null) {
            return null;
          }
    
          let result = null;
    
          // Clone TabList and Tab components to have refs
          if (count++ === 0) {
            // TODO try setting the uuid in the "constructor" for `Tab`/`TabPanel`
            result = cloneElement(child, {
              ref: 'tablist',
              children: React.Children.map(child.props.children, (tab) => {
                // null happens when conditionally rendering TabPanel/Tab
                // see https://github.com/rackt/react-tabs/issues/37
                if (tab === null) {
                  return null;
                }
    
                const ref = `tabs-${index}`;
                const id = tabIds[index];
                const panelId = panelIds[index];
                const selected = state.selectedIndex === index;
                const focus = selected && state.focus;
    
                index++;
    
                return cloneElement(tab, {
                  ref,
                  id,
                  panelId,
                  selected,
                  focus,
                });
              }),
            });
    
            // Reset index for panels
            index = 0;
          }
          // Clone TabPanel components to have refs
          else {
            const ref = `panels-${index}`;
            const id = panelIds[index];
            const tabId = tabIds[index];
            const selected = state.selectedIndex === index;
    
            index++;
    
            result = cloneElement(child, {
              ref,
              id,
              tabId,
              selected,
            });
          }
    
          return result;
        });
      }

    getChildren每次都会在render里面执行,虽然每次动态计算都会比较耗时,但这不是个大问题,真正让人担心的是里面用到的是cloneElement,cloneElement会生成新的实例对象,而这就会导致不必要的re-render(重新渲染)!!就算是银弹头pure render checking也无力挽回。

    难道一个小小的tab组件用react实现就这么复杂吗?jQuery也就没几行代码,如果是这样那还不如使用jQuery,ReactJS的组件优势又是什么。。

    现在我们回归到问题的本质,为什么要实现上面的代码?上面的代码其实是动态给组件增加props属性,例如给每个TabTitle组件添加是否selected的状态,因为组件内部无法知道selected状态,只能通过外部传入,但每个TabTitle组件又都需要这些组件,这就导致一个问题我要遍历所有TabTitle组件,然后把属性传进去。像上面的代码用在扁平结构的HTML标签倒还好,例如:

    <Tabs>
        <TabTitle to="1">
            tab1
        </TabTitle>
        <TabTitle to="2">
            tab2
        </TabTitle>
        <TabPanel for="1">
            TabPanel1
        </TabPanel>
        <TabPanel for="2">
            TabPanel2
        </TabPanel>
    </Tabs>

    但如果我要支持组件组合使用,例如下面这样:

    <Tabs onSelect={ this.onSelect } activeLinkStyle={ { color: 'red' } } defaultSelectedTab="2">
        <div>
            <TabTitle to="1">
                tab1
            </TabTitle>
        </div>
        <div>
            <TabTitle to="2">
                tab2
            </TabTitle>
        </div>
        <div>
            <TabPanel for="1">
                TabPanel1
            </TabPanel>
        </div>
        <div>
            <TabPanel for="2">
                TabPanel2
            </TabPanel>
        </div>
    </Tabs>

    上面的代码其实应用场景更广泛,因为如果你无法控制产品经理,他就会给你整这么一出!

    这样的话前面的getChildren可能就要递归遍历子元素查找,时间复杂度又增加了。

    即使解决了这么个问题,如果我的产品里一个tab里面嵌套了另一个tab,如何才能不让它们冲突呢?

    <Tab defaultSelectedTab="b">
        <TabTitle label="a">
            TabTitle a
        </TabTitle>
        <TabTitle label="b">
            TabTitle b
        </TabTitle>
        <TabTitle label="c">
            TabTitle c
        </TabTitle>
        <TabPanel for="a">
            TabPanel a
        </TabPanel>
        <TabPanel for="b">
            TabPanel b
        </TabPanel>
        <TabPanel for="c">
            <Tab>
                <TabTitle label="a">
                    TabTitle a
                </TabTitle>
                <TabTitle label="b">
                    TabTitle b
                </TabTitle>
                <TabPanel for="a">
                    TabPanel a
                </TabPanel>
                <TabPanel for="b">
                    TabPanel b
                </TabPanel>
            </Tab>
        </TabPanel>
    </Tab>

    尼玛,这也太复杂了吧!!

    如果单纯只用state和props来处理就是这样麻烦,就算是使用redux(虽然我并不推荐使用redux封装组件)也要每次自己管理全局状态。

    Context to rescue

    什么是context?

    context是react的一个高级技巧,通过它你可以不用给每个组件都传props。具体解释请看官方文档: context

    我们的根组件的context属性可以在子元素任意位置下获取到,利用这个特性我们就可以很轻易地实现上面说的组合组件和嵌套Tabs。

    实现代码的代码可以在我的github里查看到,里面还有可执行的·demo。也欢迎大家点赞~~

    我们把selectedTab放到context里面,这样子组件通过this.context.selectedTab是否和自己相同就可以推断出当前是否被激活了。

    export default class Tabs extends Component {
        constructor(props, context) {
            super(props, context);
    
            this.state = {
                selectedTab: null
            };
    
            this.firstTabLabel = null;
        }
    
        getChildContext(){
            return {
                onSelect: this.onSelect.bind(this),
                selectedTab: this.state.selectedTab || this.props.defaultSelectedTab,
                activeStyle: this.props.activeLinkStyle || defaultActiveStyle,
                firstTabLabel: this.firstTabLabel
            };
        }
    
        onSelect(tab, ...rest) {
            if(this.state.selectedTab === tab) return;
    
            this.setState({
                selectedTab: tab
            });
    
            if(typeof this.props.onSelect === 'function') {
                this.props.onSelect(tab, ...rest);
            }
        }
    
        findfirstTabLabel(children){
            if (typeof children !== 'object' || this.firstTabLabel) {
                return;
            }
    
            React.Children.forEach(children, (child) => {
                if(child.props && child.props.label) {
                    if(this.firstTabLabel == null){
                        this.firstTabLabel = child.props.label;
                        return;
                    }
                }
    
                this.findfirstTabLabel(child.props && child.props.children);
            });
        }
    
        render() {
            this.findfirstTabLabel(this.props.children);
    
            return (
                <div {...this.props}>
                    {this.props.children}
                </div>
            );
        }
    }
    Tabs.defaultProps = {
        onSelect: null,
        activeLinkStyle: null,
        defaultSelectedTab: ''
    };
    Tabs.propTypes = {
        onSelect: PropTypes.func,
        activeLinkStyle: PropTypes.object,
        defaultSelectedTab: PropTypes.string
    };
    Tabs.childContextTypes = {
        onSelect: PropTypes.func,
        selectedTab: PropTypes.string,
        activeStyle: PropTypes.object,
        firstTabLabel: PropTypes.string
    };

    上面是Tab组件的实现代码,我们在context里还增加了onSelect, activeStyle, 和firstTabLabel。

    onSelect是指我们自定义的onSelect事件, firstTabLabel主要是用来保存第一个Tab的label名称的,如果使用者没有指定默认tab就使用第一个。

    接下来是TabTitle和TabPanel的实现:

    const defaultActiveStyle = {
        fontWeight: 'bold'
    };
    
    export class TabTitle extends Component {
        constructor(props, context){
            super(props, context);
    
            this.onSelect = this.onSelect.bind(this);
        }
    
        onSelect(){
            this.context.onSelect(this.props.label);
        }
    
        componentDidMount() {
            if (this.context.selectedTab === this.props.label || this.context.firstTabLabel === this.props.label) {
                this.context.onSelect(this.props.label);
            }
        }
    
        render() {
            let style = null;
            let isActive = this.context.selectedTab === this.props.label;
            if (isActive) {
                style = this.context.activeStyle;
            }
    
            return (
                <div
                    className={ this.props.className + (isActive ? ' active' : '') }
                    style={style}
                    onClick={ this.onSelect }
                >
                    {this.props.children}
                </div>
            );
        }
    }
    TabTitle.defaultProps = {
        label: '',
        className: 'tab-link'
    };
    TabTitle.propTypes = {
        label: PropTypes.string.isRequired,
        className: PropTypes.string
    };
    TabTitle.contextTypes = {
        onSelect: PropTypes.func,
        firstTabLabel: PropTypes.string,
        activeStyle: PropTypes.object,
        selectedTab: PropTypes.string
    };
    const styles = {
        visible: {
            display: 'block'
        },
        hidden: {
            display: 'none'
        }
    };
    
    export class TabPanel extends Component {
        constructor(props, context){
            super(props, context);
        }
    
        render() {
            let displayStyle = this.context.selectedTab === this.props.for 
                ? styles.visible : styles.hidden;
    
            return (
                <div
                    className={ this.props.className }
                    style={ displayStyle }>
                    {this.props.children}
                </div>
            );
        }
    }
    TabPanel.defaultProps = {
        for: '',
        className: 'tab-content'
    };
    TabPanel.propTypes = {
        for: PropTypes.string.isRequired,
        className: PropTypes.string
    };
    TabPanel.contextTypes = {
        selectedTab: PropTypes.string
    };

    使用context后代码量少多了,而且还实现了更复杂的功能,真是一举两得。

    更多请参考我的github: https://github.com/LukeLin/react-tab/blob/master/index.js

  • 相关阅读:
    Python基础之逻辑运算符
    Python基础之赋值运算符
    Python基础之算术运算符
    Python基础之格式化输出
    Python基础之while循环
    Python基础之if语句
    Python基础之注释
    Python基础之变量和常量
    Python基础之Python解释器
    Flask-登录练习
  • 原文地址:https://www.cnblogs.com/webFrontDev/p/5631632.html
Copyright © 2020-2023  润新知