使用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>
    <TabLink to="1">
        tab1
    </TabLink>
    <TabLink to="2">
        tab2
    </TabLink>
    <TabContent for="1">
        TabContent1
    </TabContent>
    <TabContent for="2">
        TabContent2
    </TabContent>
</Tabs>

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

<Tabs>
    <div>
        <TabLink to="1">
            tab1
        </TabLink>
    </div>
    <div>
        <TabLink to="2">
            tab2
        </TabLink>
    </div>
    <div>
        <TabContent for="1">
            TabContent1
        </TabContent>
    </div>
    <div>
        <TabContent for="2">
            TabContent2
        </TabContent>
    </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.firstLink = null;
    }

    getChildContext(){
        return {
            onSelect: this.onSelect.bind(this),
            selectedTab: this.state.selectedTab || this.props.defaultSelectedTab,
            activeStyle: this.props.activeLinkStyle || defaultActiveStyle,
            firstLink: this.firstLink
        };
    }

    onSelect(tab, ...rest) {
        if(this.state.selectedTab === tab) return;

        this.setState({
            selectedTab: tab
        });

        if(typeof this.props.onSelect === ‘function‘) {
            this.props.onSelect(tab, ...rest);
        }
    }

    findFirstLink(children){
        if (typeof children !== ‘object‘ || this.firstLink) {
            return;
        }

        React.Children.forEach(children, (child) => {
            if(child.props && child.props.label) {
                if(this.firstLink == null){
                    this.firstLink = child.props.label;
                    return;
                }
            }

            this.findFirstLink(child.props && child.props.children);
        });
    }

    render() {
        this.findFirstLink(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,
    firstLink: PropTypes.string
};

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

onSelect是指我们自定义的onSelect事件, firstLink主要是用来保存第一个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.firstLink === 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,
    firstLink: 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

时间: 2024-10-09 00:49:56

使用react context实现一个支持组件组合和嵌套的React Tab组件的相关文章

初学React:定义一个组件

接着聊React,今天说说如何创建一个组件类. <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>React工程模板</title> <!-- react.js 是React的核心库 --> <script src="./build/react.js charset="

放弃antd table,基于React手写一个虚拟滚动的表格

缘起 标题有点夸张,并不是完全放弃antd-table,毕竟在react的生态圈里,对国人来说,比较好用的PC端组件库,也就antd了.即便经历了2018年圣诞彩蛋事件,antd的使用者也不仅不减,反而有所上升. 客观地说,antd是开源的,UI设计得比较美观(甩出其他组件库一条街),而且是蚂蚁金服的体验技术部(一堆p7,p8,p9,基本都是大牛级的)在持续地开发维护,质量可以信任. 不过,antd虽好,但一些组件在某一些场景下,是很不适用的.例如,以表格形式无限滚动地展示大量数据(1w+)时,

React Context

[React Context] 1.Why Not To Use Context The vast majority of applications do not need to use context. 大多数应用不需要使用context. If you want your application to be stable, don't use context. It is an experimental API and it is likely to break in future rele

[.NET] 打造一个很简单的文档转换器 - 使用组件 Spire.Office

打造一个很简单的文档转换器 - 使用组件 Spire.Office 目录 Spire.Office 介绍 库引用 界面预览 代码片段 Spire.Office 介绍 关于 Spire.Office,它是一个专门为开发人员创建,读取,写入设计的库,转换和从打印 word 文档文件.作为一个独立的 .NET组件,它不需要在机器上安装微软的 Word 等办公软件.然而,它可以将微软的“文档创建功能”集成到任何开发人员的网络应用程序中.它是一个可靠的 MS Word 的API,可以执行许多Word文档处

使用React并做一个简单的to-do-list

1. 前言 说到React,我从一年之前就开始试着了解并且看了相关的入门教程,而且还买过一本<React:引领未来的用户界面开发框架 >拜读.React的轻量组件化的思想及其visual-dom的这种技术创新,也算是早就有了初步了解.一来没有学的太深入,二来后来在工作中和业余项目中都没有用到,因此慢慢的就更加生疏了. 近期,因为我想把自己的开源项目wangEditor能放在React.angular和vuejs中使用.先从react开始,顺手自己也重试一下React的基础知识,顺便再做一个小d

SuperSwipeRefreshLayout 一个功能强大的自定义下拉刷新组件

SuperSwipeRefreshLayout 一个功能强大的自定义下拉刷新组件. Why? 下拉刷新这种控件,想必大家用的太多了,比如使用很多的XListView等.最近,项目中很多列表都是使用ReyclerView实现的,代替了原有的ListView,原有下拉刷新方式遭到挑战.本来Google推出的SwipeRefreshLayout已经能够满足大部分的需求了.然而,由于其定制性较差,下拉刷新的样式无法修改,而且被嵌套的View也无法跟随手指的滑动而滑动.基于以上考虑,定制自己强大的Supe

15. react UI组件和容器组件的拆分 及 无状态组件

1.组件的拆分 组件拆分的前提 当所有的逻辑都出现在一个组件内时 组件会变得非常复杂 不便与代码的维护 所以对组件进行拆分 IU组件 进行页面渲染 容器组件  进行逻辑操作 UI组件的拆分 新建一个 TodoListUI.js 将 TodoList 组件的 render 方法进行拆分封装为 UI 组件 其余的 TodoList 组件为 容器组件 # TodoListUI.js import  React, { Component } from 'react'; import 'antd/dist

一个支持高网络吞吐量、基于机器性能评分的TCP负载均衡器gobalan

一个支持高网络吞吐量.基于机器性能评分的TCP负载均衡器gobalan 作者最近用golang实现了一个TCP负载均衡器,灵感来自grpc.几个主要的特性就是: 支持高网络吞吐量 实现了基于机器性能评分来分配worker节点的负载均衡算法 尽量做到薄客户端,降低客户端复杂性 项目开源地址 背景 先介绍几种常用的负载均衡机制,以下几种负载均衡方案介绍来自grpc服务发现&负载均衡 根据负载均衡实现所在的位置不同,通常可分为以下四种解决方案: 集中式LB(Proxy Model) 在服务消费者和服务

从DNS基础到在CentOS6.5上&ldquo;玩着&rdquo;搭建一个支持正向、反向解析的&ldquo;

1.什么是DNS? (Domain Name System)域名系统. DNS其实实现的功能很简单也很有效,它能够让用户可以不用记得那些经常要访问服务器的ip地址,直接要你输入类似拼音格式的就可以访问到那些数字串的ip地址.假设以61.120.155.14(举个例子),我们总是用这些数字进行网页服务器的访问岂不是很蛋疼,毕竟很多人还是对文字甚至拼音字母更容易让正常人记忆.这就是DNS的功能. 当然,它不仅能把那些你输入的拼音字母转换成ip地址的数字串,它还支持把那些数字串转换成你想访问的实际的网