纵观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

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

  1. 对 React Context 的理解以及应用

    在React的官方文档中,Context被归类为高级部分(Advanced),属于React的高级API,但官方并不建议在稳定版的App中使用Context. 很多优秀的React组件都通过Conte ...

  2. React Context 的用法

    在React的官方文档中,Context被归类为高级部分(Advanced),属于React的高级API,但官方并不建议在稳定版的App中使用Context. The vast majority of ...

  3. 初学React:定义一个组件

    接着聊React,今天说说如何创建一个组件类. <!DOCTYPE html> <html lang="en"> <head> <meta ...

  4. 用 nodejs 写一个命令行工具 :创建 react 组件的命令行工具

    用 nodejs 写一个命令行工具 :创建 react 组件的命令行工具 前言 上周,同事抱怨说 react 怎么不能像 angular 那样,使用命令行工具来生成一个组件.对呀,平时工作时,想要创建 ...

  5. React Hooks 实现一个计时器组件

    React Hooks 实现一个计时器组件 useEffect https://reactjs.org/docs/hooks-reference.html#useeffect import React ...

  6. 探索 Redux4.0 版本迭代 论基础谈展望(对比 React context)

    Redux 在几天前(2018.04.18)发布了新版本,6 commits 被合入 master.从诞生起,到如今 4.0 版本,Redux 保持了使用层面的平滑过渡.同时前不久, React 也从 ...

  7. [译]React Context

    欢迎各位指导与讨论 : ) 前言 由于笔者英语和技术水平有限,有不足的地方恳请各位指出.我会及时修正的 O(∩_∩)O 当前React版本 15.0.1 时间 2016/4/25 正文 React一个 ...

  8. 如何用 React Native 创建一个iOS APP?

    诚然,React Native 结合了 Web 应用和 Native 应用的优势,可以使用 JavaScript 来开发 iOS 和 Android 原生应用.在 JavaScript 中用 Reac ...

  9. 利用React/anu编写一个弹出层

    本文将一步步介绍如何使用React或anu创建 一个弹出层. React时代,代码都是要经过编译的,我们很多时间都耗在babel与webpack上.因此本文也介绍如何玩webpack与babel. 我 ...

随机推荐

  1. C++中define与const的区别

    C++中不但可以用define定义常量还可以用const定义常量,它们的区别如下: 用#define MAX 255定义的常量是没有类型的,所给出的是一个立即数,编译器只是把所定义的常量值与所定义的常 ...

  2. Java getMethod类型参数

    public class DynamicInvoker { public static void main(String[] args) { // TODO Auto-generated method ...

  3. Domoticz 中接入斐讯 M1 空气质量检测仪

    前言 M1 是翻车讯出的一款空气质量检测仪,如今斐讯的服务器经常连不上了,M1 不动动手接到 Domoticz 怕是变成摆设了.教程参考了这里和官方的 Using Python plugins. 步骤 ...

  4. Python + gevent模块对单个接口进行并发测试 1

    本文知识点 利用gevent模块进行并发测试 代码如下 from gevent import monkey monkey.patch_all() import requests import geve ...

  5. CF914E Palindromes in a Tree(点分治)

    题面 洛谷 CF 题解 题意:给你一颗 n 个顶点的树(连通无环图).顶点从 1 到 n 编号,并且每个顶点对应一个在'a'到't'的字母. 树上的一条路径是回文是指至少有一个对应字母的排列为回文. ...

  6. nginx+uwsgi+virtualenv+supervisor部署项目

    一.导论 WSGI是Web服务器网关接口.它是一个规范,描述了Web服务器如何与Web应用程序通信,以及Web应用程序如何链接在一起以处理一个请求,(接收请求,处理请求,响应请求) 基于wsgi运行的 ...

  7. python 文件处理(基础字符)

    基于字符read & write 最基本的文件操作当然就是在文件中读写数据.这也是很容易掌握的.现在打开一个文件以进行写操作: 1. fileHandle = open ( 'test.txt ...

  8. tomcat普通用户启动不了

    Neither the JAVA_HOME nor the JRE_HOME environment variable is defined  At least one of these enviro ...

  9. oracle 日志文件

    --Oracel Grid 11.2的Agent有多个,其中有两个最重要:orarootagent.oraagent --它们有各自的日志文件,这些Agent的日志文件位于: $grid_home/l ...

  10. Long 和 Integer

    Integer 32位 其范围为 -2^31 到 2^31-1 之间,所以最大值是 2^31-1 Long 64位