使用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
使用react context实现一个支持组件组合和嵌套的React Tab组件的更多相关文章
- 对 React Context 的理解以及应用
在React的官方文档中,Context被归类为高级部分(Advanced),属于React的高级API,但官方并不建议在稳定版的App中使用Context. 很多优秀的React组件都通过Conte ...
- React Context 的用法
在React的官方文档中,Context被归类为高级部分(Advanced),属于React的高级API,但官方并不建议在稳定版的App中使用Context. The vast majority of ...
- 初学React:定义一个组件
接着聊React,今天说说如何创建一个组件类. <!DOCTYPE html> <html lang="en"> <head> <meta ...
- 用 nodejs 写一个命令行工具 :创建 react 组件的命令行工具
用 nodejs 写一个命令行工具 :创建 react 组件的命令行工具 前言 上周,同事抱怨说 react 怎么不能像 angular 那样,使用命令行工具来生成一个组件.对呀,平时工作时,想要创建 ...
- React Hooks 实现一个计时器组件
React Hooks 实现一个计时器组件 useEffect https://reactjs.org/docs/hooks-reference.html#useeffect import React ...
- 探索 Redux4.0 版本迭代 论基础谈展望(对比 React context)
Redux 在几天前(2018.04.18)发布了新版本,6 commits 被合入 master.从诞生起,到如今 4.0 版本,Redux 保持了使用层面的平滑过渡.同时前不久, React 也从 ...
- [译]React Context
欢迎各位指导与讨论 : ) 前言 由于笔者英语和技术水平有限,有不足的地方恳请各位指出.我会及时修正的 O(∩_∩)O 当前React版本 15.0.1 时间 2016/4/25 正文 React一个 ...
- 如何用 React Native 创建一个iOS APP?
诚然,React Native 结合了 Web 应用和 Native 应用的优势,可以使用 JavaScript 来开发 iOS 和 Android 原生应用.在 JavaScript 中用 Reac ...
- 利用React/anu编写一个弹出层
本文将一步步介绍如何使用React或anu创建 一个弹出层. React时代,代码都是要经过编译的,我们很多时间都耗在babel与webpack上.因此本文也介绍如何玩webpack与babel. 我 ...
随机推荐
- 导出table为Excel
1.HTML <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset=" ...
- Python博文列表
手把手|100行Python代码自动抢火车票!(包教包会):https://zhuanlan.zhihu.com/p/32928355 最全中华古诗词数据库, :https://github.com/ ...
- 谈谈easyui datagrid 的数据加载
文章目录 1url方式加载数据 1.1调用方式 1.2相关方法 1.3二次加载问题 2加载本地数据方式 2.1调用方式 2.2如何分页 2.3加载中效果 2.4如何不统计总数 这篇文章只谈jQuery ...
- 黑马学习CSS之CSS模块化规范全部组成 CSS属性列表
- springboot整合mybatis,redis,代码(一)
一 搭建项目,代码工程结构 使用idea或者sts构建springboot项目 二 数据库sql语句 SQLyog Ultimate v12.08 (64 bit) MySQL - 5.7.14-l ...
- FPGA基础学习(3) -- 跨时钟域处理方法
文章主要是基于学习后的总结. 1. 时钟域 假如设计中所有的触发器都使用一个全局网络,比如FPGA的主时钟输入,那么我们说这个设计只有一个时钟域.假如设计有两个输入时钟,如图1所示,一个时钟给接口1使 ...
- sublime text3 添加新片段
1.tools->developer->new snippet 要介绍一下snippet四个组成部分: content:其中必须包含<![CDATA[…]]>,否则无法工作, ...
- Mysql Update更新错误 Error Code:1175
Mysql 5.7,默认执行 update 语句时遇到错误提示: Error Code: 1175. You are using safe update mode and you tried to u ...
- 【算法笔记】B1036 跟奥巴马一起编程
1036 跟奥巴马一起编程 (15 分) 美国总统奥巴马不仅呼吁所有人都学习编程,甚至以身作则编写代码,成为美国历史上首位编写计算机代码的总统.2014 年底,为庆祝“计算机科学教育周”正式启动,奥巴 ...
- 【DP】【单调队列】洛谷 P2216 [HAOI2007]理想的正方形 题解
算是单调队列的复习吧,不是很难 题目描述 有一个$a\times b$的整数组成的矩阵,现请你从中找出一个$n\times n$的正方形区域,使得该区域所有数中的最大值和最小值的差最小. 输入 ...