3
votes

Wondering if there is a way we can render routes based on nested routes for each tab.

For example, when we browse to

http://localhost:3000/base/a
http://localhost:3000/base/b

We expect to have a base component which provides kind of layout and based on what tab we click it should render the nested route component.

The below example works without using react-router for rendering the tab's component.

Below is route config

<Route path={"/base:scope" } exact={true} { component: BaseComponent } />

Below component will render the page with tabs

import { Layout, Tabs, Row, Col } from 'antd';
import { Link, NavLink, Route, Switch } from 'react-router-dom';
export class BaseComponent extends React.Component {

    public state = {
        collapsed: false,
    };

    private onCollapse = collapsed => {
        this.setState({ collapsed });
    }

    get tabs() {
        const { collaborators, integration, settings, configuration, billing, tokens, referrals } = this.props;
        return [
            {
                key: 'a',
                tab: 'a',
                component: ComponentA,
                path: '/base/a',
            },
            {
                key: 'b',
                tab: 'b',
                component: ComponentB,
                path: '/base/b',
            },
        ];
    }

    public componentWillReceiveProps(props) {
        if (!_.isEqual(_.get(this.props, 'match.url'), _.get(props, 'match.url'))) {
            this.setState({ key: _.get(props, 'match.url') });
        }
    }

    public render() {
        const { renderer, router } = this.context;
        const { onLogOut, match, user, profile } = this.props;

        return (
            <React.Fragment>
                <Header className={renderer.renderRule(styles.header, this.props)}>
                    <div className={renderer.renderRule(styles.title, this.props)}>
                        Account Settings
                    </div>
                </Header>
                <Content>
                    <div className={renderer.renderRule(styles.container, this.props)}>
                        <Tabs
                            animated={false}
                            defaultActiveKey={match.url}
                            onChange={key => router.history.push(key)}
                            className={`${renderer.renderRule(styles.tabs)}`}
                        >
                            {_.map(this.tabs, (record, index) => (
                                <TabPane
                                    key={record.path}
                                    className={renderer.renderRule(styles.pane)}
                                    tab={<span>{record.tab}</span>}
                                >
                                    {React.createElement(record.component, null, null)}
                                </TabPane>

                            ))}
                        </Tabs>
                    </div>
                </Content>
            </React.Fragment >
        );
    }
}

Expectation:

We want to write to be more react-router specific like

<Routes path={"/base"} exact={false} component={ComponentBase} />
<Routes path={"/base/a"} exact={true} component={ComponentBase} />
<Routes path={"/base/b"} exact={true} component={ComponentBase} />

But in this case we don't know how to render the page as the react-router not rending the page so we notice the tabs but no render content.

Here is the modified component without render content as we expect react-router to render the component.

export class BaseComponent extends React.Component {

    public state = {
        collapsed: false,
    };

    private onCollapse = collapsed => {
        this.setState({ collapsed });
    }

    get tabs() {
        const { collaborators, integration, settings, configuration, billing, tokens, referrals } = this.props;
        return [
            {
                key: 'a',
                tab: 'a',
                path: '/base/a',
            },
            {
                key: 'b',
                tab: 'b',
                path: '/base/b',
            },
        ];
    }

    public componentWillReceiveProps(props) {
        if (!_.isEqual(_.get(this.props, 'match.url'), _.get(props, 'match.url'))) {
            this.setState({ key: _.get(props, 'match.url') });
        }
    }

    public render() {
        const { renderer, router } = this.context;
        const { onLogOut, match, user, profile } = this.props;

        return (
            <React.Fragment>
                <Header className={renderer.renderRule(styles.header, this.props)}>
                    <div className={renderer.renderRule(styles.title, this.props)}>
                        Account Settings
                    </div>
                </Header>
                <Content>
                    <div className={renderer.renderRule(styles.container, this.props)}>
                        <Tabs
                            animated={false}
                            defaultActiveKey={match.url}
                            onChange={key => router.history.push(key)}
                            className={`${renderer.renderRule(styles.tabs)}`}
                        >
                            {_.map(this.tabs, (record, index) => (
                                <TabPane
                                    key={record.path}
                                    className={renderer.renderRule(styles.pane)}
                                    tab={<span>{record.tab}</span>}
                                >
                                </TabPane>

                            ))}
                        </Tabs>
                    </div>
                </Content>
            </React.Fragment >
        );
    }
}
2

2 Answers

1
votes

I tried to created RoutedTabs component using antd Tabs. I have added docstring for the same.

import React from "react";
import PropTypes from "prop-types";
import { Tabs } from "antd";
import { Switch, Route } from "react-router-dom";

import _each from "lodash/each";
import _map from "lodash/map";
const { TabPane } = Tabs;
/**
 * RoutedTabs is Ant Design based dynamic navigation tabs component.
 * It takes router config as prop and render the component as well as update the window location corrosponding to the tab.
 * Example of routeConfig
 * overview: {
        label: "Overview",
        component: RestaurantOverview,
        getRoute: url => url
    },
    menu: {
        label: "Menu",
        component: Menu,
        getRoute: url => `${url}/menu`
    },
    "menu-holiday-slot": {
        label: "Menu Holiday Slot",
        component: MenuHolidaySlots,
        getRoute: url => `${url}/menu-holiday-slot`
    }
    This will render three tabs Overview, Menu, Menu Holiday Slot and routes based on what getRoute method returns.
 */
const RoutedTabs = props => {
    const { tabsProps, routeConfig } = props;
    const { url, path } = props.match;
    const tabToRouteMap = {};
    const routeToTabsMap = {};
    _each(routeConfig, (configObj, routeKey) => {
        const routeURL = configObj.getRoute(url);
        tabToRouteMap[routeKey] = routeURL;
        routeToTabsMap[routeURL] = routeKey;
    });
    const defaultActiveKey = routeToTabsMap[props.history.location.pathname];
    const tabPaneNodes = _map(routeConfig, (configObj, routeKey) => (
        <TabPane tab={configObj.label} key={routeKey} />
    ));
    const routeNodes = _map(routeConfig, (configObj, routeKey) => (
        <Route
            path={configObj.getRoute(path)}
            exact
            key={routeKey}
            component={configObj.component}
        />
    ));
    const onTabChange = activeKey => {
        props.history.push(tabToRouteMap[activeKey]);
    };
    return (
        <>
            <Tabs {...tabsProps} onChange={onTabChange} defaultActiveKey={defaultActiveKey}>
                {tabPaneNodes}
            </Tabs>
            <Switch>{routeNodes}</Switch>
        </>
    );
};

RoutedTabs.propTypes = {
    tabsProps: PropTypes.object.isRequired, // As per https://ant.design/components/tabs/#Tabs
    routeConfig: PropTypes.object.isRequired,
    match: PropTypes.object.isRequired,
    history: PropTypes.object.isRequired
};

export default RoutedTabs;
-2
votes

If you are certain you want to define the tabs explicitly with router then you set a prop in the component:

<Route path={"/base/a"} exact={true} render={() => <ComponentBase tab="a" />} />

More info on Route from react router docs.

I think you would be better off using props.match.paramsthat is provided by react router instead of explicitly defining the tabs, more info and example here.

You can than get the value off of {props.match.params.base} and pull that tab.