使用react context實現一個支持組件組合和嵌套的React Tab組件

縱觀react的tab組件中,即便是github上star數多的tab組件,實現原理都很是冗餘。html

例如Github上star數超四百星的react-tab,其在render的時候都會動態計算哪一個tab是被選中的,哪一個該被隱藏:react

  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也無力挽回。git

 

難道一個小小的tab組件用react實現就這麼複雜嗎?jQuery也就沒幾行代碼,若是是這樣那還不如使用jQuery,ReactJS的組件優點又是什麼。。github

 

如今咱們迴歸到問題的本質,爲何要實現上面的代碼?上面的代碼實際上是動態給組件增長props屬性,例如給每一個TabTitle組件添加是否selected的狀態,由於組件內部沒法知道selected狀態,只能經過外部傳入,但每一個TabTitle組件又都須要這些組件,這就致使一個問題我要遍歷全部TabTitle組件,而後把屬性傳進去。像上面的代碼用在扁平結構的HTML標籤倒還好,例如:redux

<Tabs>
    <TabTitle to="1">
        tab1
    </TabTitle>
    <TabTitle to="2">
        tab2
    </TabTitle>
    <TabPanel for="1">
        TabPanel1
    </TabPanel>
    <TabPanel for="2">
        TabPanel2
    </TabPanel>
</Tabs>

 

但若是我要支持組件組合使用,例以下面這樣:app

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

 

上面的代碼其實應用場景更普遍,由於若是你沒法控制產品經理,他就會給你整這麼一出!ui

這樣的話前面的getChildren可能就要遞歸遍歷子元素查找,時間複雜度又增長了。this

 

即便解決了這麼個問題,若是個人產品裏一個tab裏面嵌套了另外一個tab,如何才能不讓它們衝突呢?spa

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

 

尼瑪,這也太複雜了吧!!rest

若是單純只用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

相關文章
相關標籤/搜索