How to match multiple sibling routes with React Router V6?

Viewed 133

I am trying to implement tabs navigation using React router v6. To do so, I need the following rendering pattern:

  • Render <Tabs /> and <TabPanelA /> when URL is /tabsDemo/a
  • Render <Tabs /> and <TabPanelB /> when URL is /tabsDemo/b

<Tabs /> is a navigation bar that allows users to navigate between tab panels A and B. It needs to be aware of currentTab so that it can change the corresponding tab's color. It also needs to be constantly rendered even as the user navigates across different tab panels.

However, my current code only renders <TabPanelA /> when the URL is /tabsDemo/a because /tabsDemo/a is more specific than /tabsDemo/:currentTab

function App() {
  return (
    <Routes>
      <Route path="tabsDemo" element={<Outlet />}>
        <Route path=":currentTab" element={<Tabs />} />
        <Route path="a" element={<TabPanelA />} />
        <Route path="b" element={<TabPanelB />} />
      </Route>
    </Routes>
  );
}

How can I achieve the aforementioned rendering pattern?

3 Answers

The most trivial method would be to render the Tabs component in each place you want.

Example:

function App() {
  return (
    <Routes>
      <Route path="tabsDemo" element={<Outlet />}>
        <Route path=":currentTab" element={<Tabs />} />
        <Route
          path="a"
          element={(
            <>
              <Tabs />
              <TabPanelA />
            </>
          )}
        />
        <Route
          path="b"
          element={(
            <>
              <Tabs />
              <TabPanelB />
            </>
          )}
        />
      </Route>
    </Routes>
  );
}

It sounds like you want to render the Tabs component regardless. For this you should convert it into a layout route that renders the Outlet.

Example:

const Tabs = () => {
  ... tabs logic ...

  return (
    ...
    <Outlet />
    ...
  );
};

...

function App() {
  return (
    <Routes>
      <Route path="tabsDemo" element={<Tabs />}>
        <Route path="a" element={<TabPanelA />} />
        <Route path="b" element={<TabPanelB />} />
        <Route
          path=":currentTab"
          element={/* some component to read the `currentTab` param ??? */}
        />
      </Route>
    </Routes>
  );
}

NOTE: When using params in nested routes with an indexed route, the two routes below are similar to ReactRouter.

  • indexed Route A: "TabsDemo/:a"
  • Route B: "TabsDemo/:b"

You can try going to "TabsDemo/abcabc" and it will still route to the indexed Route A.

If you're desperate , you can use this for now. I'll update with a propery way when I can.

import { Outlet, Routes, Route, useLocation } from "react-router-dom";

import { useState, useEffect } from "react";

export default function App() {
  
  function TabsDemo() {
    return (
      <>
        <div>Tabs Demo</div>
        <Outlet />
      </>
    );
  }

  function Tabs() {
    const location = useLocation();
    const [curentTab, setCurrentTab] = useState();

    useEffect(() => {
      let pathString = location.pathname;
      // This will give you the current tab
      let currentTab = pathString.includes("/tabs/b") ? "b" : "a";
      setCurrentTab(currentTab);          
    }, [location]);

    return (
      <>
        <div>
         <div>Tabs</div> 
         <div>{currentTab}</div>
        </div>
        <Outlet />
      </>
    );
  }

  function TableA() {
    return <div>Table A</div>;
  }

  function TableB() {
    return <div>Table B</div>;
  }

  return (
    <Routes>
      <Route exact path="/tabsDemo" element={<TabsDemo />}>
        <Route exact path="tabs" element={<Tabs />}>
          <Route index={true} element={<TableA />} />
          <Route path="b" element={<TableB />} />
        </Route>
      </Route>
    </Routes>
  );
}
Related