reactjs / react-tabs

An accessible and easy tab component for ReactJS.
https://reactcommunity.org/react-tabs/
MIT License
3.08k stars 446 forks source link

[Feature Request] Support for Functional Components (Dynamic Tabs) #245

Closed lucas-viewup closed 2 years ago

lucas-viewup commented 6 years ago

Hello, I'm using the react-tabs and is working well but I feel a little delay on switching the tabs, I suppose that is because the size of the render of my component:

import classNames from 'classnames';

import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from 'material-ui/styles';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';

const borderStyle = '1px solid rgba(0, 0, 0, 0.1)';
const styles = {
    root: {
        margin: '10px 0',
    },
    primaryTab: {
        fontFamily: 'Roboto',
        fontSize: '0.75em',
        marginRight: 24,
        marginBottom: 4,
        padding: '8px 32px',
        borderRadius: '2em',
        border: 'none',
        transition: `background-color 200ms,
        color 200ms`,
        '&.react-tabs__tab--selected': {
            background: '#F5A623',
            color: '#fff',
        }
    },
    primaryTabList: {
        borderBottom: borderStyle,
    },
    secondaryTab: {
        fontFamily: 'Karla',
        fontWeight: 700,
        fontSize: '0.75em',
        textTransform: 'uppercase',
        padding: '8px 48px',
        border: 'none',
        borderRadius: 2,
        bottom: 0,
        transition: `background-color 200ms,
        color 200ms`,
        '&.react-tabs__tab--selected': {
            background: '#3ACA60',
            color: '#fff',
        }
    },
    secondaryTabList:{
        border: borderStyle,
        padding: 0,
        borderRadius: 2,
    },
    tertiaryTab: {
        padding: '8px 12px',
        fontFamily: 'Karla',
        borderRadius: 0,
        fontWeight: 700,
        bottom: 0,
        fontSize: '0.75em',
        border: 0,
        '&.react-tabs__tab--selected': {
            background: '#E2E2E2',
        }
    },
    tertiaryTabList: {
        borderRadius: 4,
        display: 'inline-block',
        border: borderStyle,
        margin: 0,
        '& :not(:last-child)':{
            borderRight: borderStyle,
        }
    },

};

class CustomTabs extends React.PureComponent{

    render(){
        const {classes, tabs, type, tabProps, tabListProps, tabPanelProps, tabsCallback, tabsProps, ...other} = this.props;

        let tabItems = [];
        let tabPanels = [];
        let renderedTabs;
        let tabList;

        const tabDefaults = Tab.defaultProps;
        const tabListDefaults = TabList.defaultProps;

        tabs.map( (item, index) => {
            tabItems.push(
                <Tab key={index} className={ classNames(tabDefaults.className, classes[`${type}Tab`]) } {...tabProps} >{item.label}</Tab>
            );
        });

        tabs.map( (item, index) => {
            tabPanels.push(
                <TabPanel key={index} {...tabPanelProps}>{item.component}</TabPanel>
            );
        });

        tabList = (
            <TabList className={classNames(tabListDefaults.className, classes[`${type}TabList`])} {...tabListProps}>
                {tabItems}
            </TabList>
        );

        if(tabsCallback){
            renderedTabs = tabsCallback(tabList, tabPanels);
        }
        else{
            renderedTabs = [
                tabList,
                tabPanels
            ];
        }

        return (
            <div className={classes.root} {...other}>
                <Tabs {...tabsProps}>
                    {renderedTabs}
                </Tabs>
            </div>
        );
    }

};

const tabShape = {
    label: PropTypes.string,
    component: PropTypes.node,
};  

CustomTabs.propTypes = {
    type: PropTypes.oneOf(['primary', 'secondary', 'tertiary']),
    tabs: PropTypes.array,
    tabProps: PropTypes.object,
    tabListProps: PropTypes.object,
    tabPanelProps: PropTypes.object,
    tabsCallback: PropTypes.func, 
    tabsProps: PropTypes.object,
};

CustomTabs.defaultProps = {
    type: 'primary',
    tabs: [],
};  

export default withStyles(styles)(CustomTabs);

In a attempt to optimize the performance of this, I tried to split in two components, a component to render the TabList with the Tabs and a component to render the TabPanels:

import classNames from 'classnames';

import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from 'material-ui/styles';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';

const borderStyle = '1px solid rgba(0, 0, 0, 0.1)';
const styles = {
    root: {
        margin: '10px 0',
    },
    primaryTab: {
        fontFamily: 'Roboto',
        fontSize: '0.75em',
        marginRight: 24,
        marginBottom: 4,
        padding: '8px 32px',
        borderRadius: '2em',
        border: 'none',
        transition: `background-color 200ms,
        color 200ms`,
        '&.react-tabs__tab--selected': {
            background: '#F5A623',
            color: '#fff',
        }
    },
    primaryTabList: {
        borderBottom: borderStyle,
    },
    secondaryTab: {
        fontFamily: 'Karla',
        fontWeight: 700,
        fontSize: '0.75em',
        textTransform: 'uppercase',
        padding: '8px 48px',
        border: 'none',
        borderRadius: 2,
        bottom: 0,
        transition: `background-color 200ms,
        color 200ms`,
        '&.react-tabs__tab--selected': {
            background: '#3ACA60',
            color: '#fff',
        }
    },
    secondaryTabList:{
        border: borderStyle,
        padding: 0,
        borderRadius: 2,
    },
    tertiaryTab: {
        padding: '8px 12px',
        fontFamily: 'Karla',
        borderRadius: 0,
        fontWeight: 700,
        bottom: 0,
        fontSize: '0.75em',
        border: 0,
        '&.react-tabs__tab--selected': {
            background: '#E2E2E2',
        }
    },
    tertiaryTabList: {
        borderRadius: 4,
        display: 'inline-block',
        border: borderStyle,
        margin: 0,
        '& :not(:last-child)':{
            borderRight: borderStyle,
        }
    },

};

const TabItems = (props) => {

    const {classes, tabs, type, tabProps, tabListProps} = props;
    const tabDefaults = Tab.defaultProps;
    const tabListDefaults = TabList.defaultProps;

    const tabItems = tabs.map( (item, index) => (
        <Tab key={index} className={ classNames(tabDefaults.className, classes[`${type}Tab`]) } {...tabProps} >{item.label}</Tab>
    ) );

    console.log({tabs, tabItems});

    return (
        <TabList className={classNames(tabListDefaults.className, classes[`${type}TabList`])} {...tabListProps}>
            {tabItems}
        </TabList>
    );
};

const TabPanels = (props) => {

    const {classes, tabs, tabPanelProps} = props;

    return tabs.map( (item, index) => (
        <TabPanel key={index} {...tabPanelProps}>{item.component}</TabPanel>
    ) );
};

class CustomTabs extends React.PureComponent{

    renderTabs = () => {

        const {classes, type, tabs, tabPanelProps, tabProps, tabListProps, tabsCallback} = this.props;
        const tabList = <TabItems 
            classes={classes} 
            type={type} 
            tabs={tabs} 
            tabProps={tabProps}
            tabListProps={tabListProps} />;

        const tabPanels = <TabPanels 
            classes={classes} 
            tabPanelProps={tabPanelProps} 
            tabs={tabs}  />;

        return tabsCallback ? tabsCallback(tabList, tabPanels) : [tabList, tabPanels];
    }

    render(){
        const {classes, tabs, type, tabProps, tabListProps, tabPanelProps, tabsCallback, tabsProps, ...other} = this.props;

        return (
            <div className={classes.root} {...other}>
                <Tabs {...tabsProps}>
                    {this.renderTabs()}
                </Tabs>
            </div>
        );
    }

};

const tabShape = {
    label: PropTypes.string,
    component: PropTypes.node,
};  

CustomTabs.propTypes = {
    type: PropTypes.oneOf(['primary', 'secondary', 'tertiary']),
    tabs: PropTypes.array,
    tabProps: PropTypes.object,
    tabListProps: PropTypes.object,
    tabPanelProps: PropTypes.object,
    tabsCallback: PropTypes.func, 
    tabsProps: PropTypes.object,
};

CustomTabs.defaultProps = {
    type: 'primary',
    tabs: [],
};  

export default withStyles(styles)(CustomTabs);

The fact is that the second component didn't work, I guess is because of the wrapper the functional component creates in the React DOM. I had this same problem with the tabs of material-ui either. Maybe some key-value prop could solve this issue? Here is some example:

<Tab tabId="my-id" /> ... <TabPanel tabId="my-id" />

joepvl commented 6 years ago

From the code you posted, I can't easily make out what you're trying to do, or in what way it isn't working. Creating an example on e.g. https://codesandbox.io that shows what you're running into would help us and others help you.

That said, you're right, that component wrapper will cause an issue — the short of it is that <Tabs> won't be able to find the <TabPanel>s in its descendants because their rendering is deferred to a different component. Unfortunately, the why and how of this behaviour are kind of integral to how react-tabs works right now, so you're not likely to see support for your case soon.

Regardless, I suggest questioning your assumption with respect to why you're experiencing a delay when switching tabs. From what I'm reading, I think you're suspecting unnecessary updates/rerendering to be the culprit, which is probably a good guess, but without using any dev tools to find out where exactly the delay is coming from it's hard to figure out where it's happening. I also don't think wrapping your panel rendering within a separate component, had it been an option in this case, would have improved perf. The rendering would still need to happen — simply wrapping the components wouldn't prevent that. In fact, you'd add another layer of rendering, so technically it would be more work for the renderer! But it wouldn't make a practical difference.

lucas-viewup commented 6 years ago

Thanks for feedback and hints, @joepvl! As soon as I get some time, I will create an sample https://codesandbox.io. I will do some tests in that platform and see if i will experience the same issues I'm having in a real application and share with you. If we could do, let's do it better then!

Regards,

joepvl commented 6 years ago

@lucas-viewup any updates on this?