使用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. 我 ...
随机推荐
- ansible基本模块-cron
ansible XXX -m cron -a "name=‘XXX ’ job=‘执行的命令’ minute=XXX " ...
- adminlte+layui框架搭建3 - layui弹出层
在amdinlte首页引入layui.js 和layui.css后添加代码 <script> layui.use(['layer'], function () { var layer = ...
- 14. 异步加载Js的方式有哪些?
我们都知道渲染引擎遇到 script 标签会停下来,等到执行完脚本,继续向下渲染,如下: <script type="text/javascript" src=". ...
- 老实pear_Excel 操作类 Spreadsheet_Excel_Writer 常用参数说明
(如果是PHP5项目就不用往下看了,因为PHP5项目可以直接用PHPExcel,方便快捷) 手上有个PHP4的修改项目,要修改Excel的导出,然后再把导出的Excel再导入到系统里. 在导入的时候, ...
- Rails应用系列(1):初识Rails
第一个Rails应用 Rails是一个"模型-视图-控制器"框架(MVC).是用Ruby写的,所以要对Ruby要有一定的了解才能对rails框架深入学习.其实Ruby与Rails就 ...
- C#异步编程之基于任务的异步模式
http://www.cnblogs.com/afei-24/p/6757361.html该文讲了基于任务的编程,这里再详细介绍一下.一.延续任务 private async static void ...
- ZOJ - 2042 模运算DP
解法见网上参考 这种只判断可达性的DP一般用bool 除非int能得到更多的信息 #include<iostream> #include<algorithm> #include ...
- linux下忘记mysql的root密码
一.处理方案 #1. 结束当前正在运行的mysql进程 /etc/init.d/mysql stop #2. 用mysql安全模式运行并跳过权限验证 mysqld_safe --user=mysql ...
- Robot Framework变量的使用技巧
1.变量的使用 变量可以在命令行中设置,个别变量设置使用--variable (-v)选项,变量文件的选择使用--variablefile (-V)选项.通过命令行设置的变量是全局变量,对其所有执行的 ...
- 2.rabbitmq 系列教程
rabbitmq系列教程-文章[转] 视频分享: 链接:https://pan.baidu.com/s/1s_Qr2A1o0s8Ru0exK62jqg 提取码:eb68