纵观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. jenkins官方文档(中文版)

    https://www.w3cschool.cn/jenkins/

  2. charles破解激活方法,注册码

    1  最简单的,就是买一个激活码,在网上找到一个,记录一下. // Charles Proxy License // 适用于Charles任意版本的注册码,谁还会想要使用破解版呢. // Charle ...

  3. day44--浮动和清浮动基本小结

    浮动和清浮动 一:浮动 浮动元素会生成一个块级框,而不论它本身是何种元素. 关于浮动的两个特点: 浮动的框可以向左或向右移动,直到它的外边缘碰到包含框或另一个浮动框的边框为止. 由于浮动框不在文档的普 ...

  4. IIS 配置 url 重写...

    <?xml version="1.0"?> <configuration> <system.webServer> <rewrite> ...

  5. Codeforces Round #462 (Div. 2), problem: (C) A Twisty Movement (求可以转一次区间的不递增子序列元素只有1,2)

    题目意思: 给长度为n(n<=2000)的数字串,数字只能为1或者2,可以将其中一段区间[l,r]翻转,求翻转后的最长非递减子序列长度. 题解:求出1的前缀和,2的后缀和,以及区间[i,j]的最 ...

  6. gitlab 日常运维命令

    2018-10-24 gitlab查看版本 cat /opt/gitlab/embedded/service/gitlab-rails/VERSION 查看gitlab服务状态命令 gitlab-ct ...

  7. c语言3种方式实现参数传递

    学习计算机已经两年了,参数传递已经成功恶心了我两年,今天在写二叉树遍历的时候成功对此忍无可忍.本文是在阅读https://blog.csdn.net/u013097971/article/detail ...

  8. CenctOS6 and CenctOS7 多种姿势解决忘记密码

    -----linux---- 忘记密码啦!!! 忘记密码教程!!! 教你们忘记密码(我原来密码就是123456,忘记是不可能的!假装忘记的样子 0.0) 现在我们忘记密码了!对忘记密码了.我忘记密码了 ...

  9. Redis启动和关闭

    带配置文件启动 ./redis-server redis.conf 关闭 无密码模式  ./redis-cli -h xxx -p xxx shutdown 密码模式  ./redis-cli -h ...

  10. linus系统下使用hexo搭建个人博客

    最近在搭建自己的个人网站,准备在上面内置一个博客模块,把之前或者以后杂七杂八的总结都放里边. 大致查了一下在WordPress和Hexo间选用了Hexo,体量较小一点. 先贴上Hexo的官方文档:He ...