纵观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. 【算法笔记】B1036 跟奥巴马一起编程

    1036 跟奥巴马一起编程 (15 分) 美国总统奥巴马不仅呼吁所有人都学习编程,甚至以身作则编写代码,成为美国历史上首位编写计算机代码的总统.2014 年底,为庆祝“计算机科学教育周”正式启动,奥巴 ...

  2. 2017西安区域赛A / UVALive - 8512 线段树维护线性基合并

    题意:给定\(a[1...n]\),\(Q\)次询问求\(A[L...R]\)的异或组合再或上\(K\)的最大值 本题是2017的西安区域赛A题,了解线性基之后你会发现这根本就是套路题.. 只要用线段 ...

  3. ansys 14.0

    张建伟等.2012.12 人民邮电 一天一本书,这种感觉 真是爽啊. 但我觉得什么也没学到. 话说回来,此authors 确实书写得比较一般,实例不如百度文档上的WORD实例,理论不如我看的那本CFD ...

  4. Apache Shiro(二)-登录认证和权限管理数据库操作

    数据库支持 在上一篇中使用ini 配置文件进行了相关权限数据的配置. 但是实际工作中,我们都会把权限相关的内容放在数据库里. 所以本知识点讲解如何放在数据库里来撸. RBAC 概念 RBAC 是当下权 ...

  5. [转] Java:对Scanner的useDelimiter()方法的疑问

    [From]https://segmentfault.com/q/1010000003885362 Windows下,我们在键盘上按下Enter键,实际上输入的是回车和换行两个字符:\r\n,ASCI ...

  6. 完美的js继承

    //完美的js继承 少了类管理器 Object.extend=function(){ var fnTest = /\b_super\b/; //继承父类 var _super = arguments[ ...

  7. PostgreSQL精简命令:

    dos命令行连接PostgreSQL: . 接入PostgreSQL数据库: psql -h IP地址 -p 端口 -U 用户名 -d 数据库名 . 输入数据库密码 C:\Users\admin\De ...

  8. PIE SDK打开Micaps数据

    1. 功能简介 Micaps数据是气象信息处理和天气预报制作中的一种气象数据格式.其包含多种气象信息产品(地面常规气象观测数据产品.高空常规气象观测数据产品等),目前Micaps数据被Micaps软件 ...

  9. Hash算法总结

    1. Hash是什么,它的作用 先举个例子.我们每个活在世上的人,为了能够参与各种社会活动,都需要一个用于识别自己的标志.也许你觉得名字或是身份证就足以代表你这个人,但是这种代表性非常脆弱,因为重名的 ...

  10. 016-hibernateutils模板

    package ${enclosing_package}; import org.hibernate.HibernateException; import org.hibernate.Session; ...