Search code examples
tabsreact-routerreact-router-v4react-router-dommulti-level

React-router 4 in browser, how to make multilevel tab menu?


I'm using react-router-dom and react-router-config v4 in a browser app. I need to have a multilevel tab menu from graph of routes (each route either has a component to render or child routes to be shown as next level of menu).

I'd like to get following menu structure:

multilevel menu

From a decsription like this:

 const routes = [
  {
    path: "/Tab1",
    name: "Tab 01"
    component: Tab1
  },
  {
    path: "/Tab2",
    name: "Tab 02"
    component: Tab12
  },
  {
    path: "/Tab3",
    name: "Tab 03"
    component: null,
    routes: [
      {
        path: "/Tab3/SubTab1",
        name: "SubTab 01"
        component: SubTab1
      },
      {
        path: "/Tab3/SubTab2",
        name: "SubTab 02"
        component: SubTab2
      },
      {
        path: "/Tab3/SubTab3",
        name: "SubTab 03"
        component: null,
        routes: [
         ...
        ]
      },
    ]
  },
  ...
];

Solution

  • I came up with a satisfactory solution (following TypeScript snippet is lengthy, but most of it is route graph definition).

    import * as React from "react";
    import * as ReactDOM from "react-dom";
    import { BrowserRouter, Route, Link, } from "react-router-dom";
    import { renderRoutes, RouteConfig, MatchedRoute } from 'react-router-config'
    import { Location } from "history";
    
    declare module 'react-router-config'{
        interface RouteConfig{
            tabName?: string;
            defaultSubpath?: string;
        }
    
        interface MatchedRoute<T>{
            location: Location;
        }
    }
    
    // todo optimize with memoization?
    function getActiveRoutes(match: MatchedRoute<any>):RouteConfig[]{
    
      const currentPath = match.location.pathname;
      const routes = match.route.routes;
    
      let activeRoutes:RouteConfig[] = [];
    
      fillActiveRoutes(routes);
    
      return activeRoutes;
    
      function fillActiveRoutes(current: RouteConfig[]){
        for(const route of current){
    
          activeRoutes.push(route);
          let isActive = false;
    
          if(!route.routes || route.routes.length === 0){
            isActive = route.path === currentPath;
          } else if(route.routes) {
            let isActive = fillActiveRoutes(route.routes);
          }
    
          if(isActive === false){
            activeRoutes.pop();
          } else {
            break;
          }
        }
      }
    }
    
    const ChildLinks = (match: MatchedRoute<any>) => {
      let activeRoutes = getActiveRoutes(match);
    
      return(<div>
                {match.route.routes.map((route) => {
                      let isActive = activeRoutes.some(x => x === route);
                      let to = route.defaultSubpath || route.path;
                      let key = 'main-tabs-link-' + route.path;
                      let label = isActive ? `  [${route.tabName}]  ` : `  ${route.tabName}  `;
                      return (<Link to={to} key={key}> {label} </Link>);
                    })
                }
            </div>);
    }
    
    const EmptyRenderer:React.StatelessComponent<MatchedRoute<any>> = (match: MatchedRoute<any>) => (<div>
      {ChildLinks(match)}
      {renderRoutes(match.route.routes)}
    </div>);
    
    const Root:React.StatelessComponent<MatchedRoute<any>> = (match: MatchedRoute<any>) =>  (<div>
        <h1>Root</h1>
        { EmptyRenderer(match) }
      </div>);
    
    const StaticDiv: (content:string) => React.StatelessComponent<MatchedRoute<any>> = (content:string) => 
            () => (<div>{content}</div>)
    
    const routes:RouteConfig[]  = [
      { component: Root,
        routes: [
          { path: '/A/',
            tabName: 'A',
            exact: true,
            component: StaticDiv("A")
          },
          { path: '/B/',
            tabName: 'B',
            defaultSubpath: '/B/2/',
            exact: false,
            component: EmptyRenderer,
            routes: [
                { 
                path: '/B/1/',
                exact: true,
                tabName: "B1",
                component: StaticDiv("B1")
                },{ 
                path: '/B/2/',
                exact: true,
                tabName: "B2",
                component: StaticDiv("B2")
              },{ 
                path: '/B/3/',
                exact: true,
                tabName: "B3",
                component: StaticDiv("B3")
            }]
          },
          { path: '/C/',
            tabName: 'C',
            defaultSubpath: '/C/3/Z/',
            exact: false,
            component: EmptyRenderer,
            routes: [
                { 
                path: '/C/1/',
                exact: true,
                tabName: "C1",
                component: StaticDiv("C1")
                },{ 
                path: '/C/2/',
                exact: true,
                tabName: "C2",
                component: StaticDiv("C2")
              },{ 
                path: '/C/3/',
                defaultSubpath: '/C/3/Z/',
                exact: false,
                tabName: "C3",
                component:EmptyRenderer,
                routes: [
                    { 
                    path: '/C/3/X/',
                    exact: true,
                    tabName: "C3X",
                    component: StaticDiv("C3X")
                    },{ 
                    path: '/C/3/Y/',
                    exact: true,
                    tabName: "C3Y",
                    component: StaticDiv("C3Y")
                  },{ 
                    path: '/C/3/Z/',
                    exact: true,
                    tabName: "C3Z",
                    component: StaticDiv("C3Z")
                }]
            }]
          }
        ]
      }
    ]
    
    export const Example = () => (<BrowserRouter>
        {renderRoutes(routes)}
      </BrowserRouter>);
    

    Lib versions used:

    "dependencies": {
        "react": "^16.2.0",
        "react-dom": "^16.2.0",
        "react-router": "^4.2.0",
        "react-router-config": "^1.0.0-beta.4",
        "react-router-dom": "^4.2.2"
      },
      "devDependencies": {
        "@types/react": "^16.0.38",
        "@types/react-dom": "^16.0.4",
        "@types/react-router-config": "^1.0.6",
        "typescript": "^2.7.1"
      }