typescriptreact-routerreact-router-dom

React router V6: how to have two simultaneous and independent navigation outlets


I am building a website with a 2 parts (let's call them left and right) and on each part there is tabs.

Something like this : enter image description here

What I would like to have is an URL like this :

"https://mywebsite?right=billing&left=stock"

where 'left=' and 'right=' are used to find the content to display.

Simplified App structure

I have a Split component where there is PanelLeft and PanelRight children components:

App.tsx

<Split
  sizes={60, 40}
  direction="horizontal"
>
  <PanelLeft />
  <PanelRight />
</Split>

const PanelLeft = () => {
  const leftPart: string = extractQueryFromUrl('left') // give me 'stock'
  return <Outlet context={{ leftPart }} />;
};

const PanelRight= () => {
  const rightPart: string = extractQueryFromUrl('right') // give me 'billing'
  return <Outlet context={{ rightPart }} />;
};

main.tsx

const router = createBrowserRouter([
  {
    path: "/",
    element: <App/>,
    children: [
      {
        path: "",
        element: <PanelLeft />,
        children: generateChildRoutes(),
      },
      {
        path: "",
        element: <PanelRight />,
        children: generateChildRoutes(),
      },
    ],
  },
]);

const generateChildRoutes = () => [
  { path: "/", element: <Home /> },
  { path: "stock", element: <Stock /> },
  { path: "billing", element: <Billing /> },
];

createRoot(document.getElementById("root")!).render(
  <RouterProvider router={router}>
  </RouterProvider>
);

Obviously, the mapping in createBrowserRouter and the <Outlet context={{leftPart}}/> are not working at all.

I wonder how could I configure them to be able to get the two independent navigation panel.

Note: The real app is much more complicated, use redux to retrieve states based on unique tabId etc... It is working very well with one Outlet and for instance two sub routes (https://mywebsite/panelright/billing and https://mywebsite/panelleft/billing) so the issue is really for simultaneous independent display.

EDIT (22/09/2024)

Thank you both @Drew Reese and @Matthew for your time and explanations. You were right that having multiple Outlets makes no sense and will never work as intended. I ended up with a solution close to Matthew's suggestion with one outlet and direct use on the other.

something like this

export const tabsRoutesMap: Map<string, JSX.Element> = new Map([
    [Billing_Path, <Billing />],
    [Stock_Path, <Stock/>],
//... others possible tabs...
]);

const router = createBrowserRouter([
    {
        path: "/",
        element: <App/>,
        // generate children from tabs route
        children: Array.from(tabsRoutesMap, ([path, element]) => ({ path, element })),
    
    },
]);

And then :

<>
    {tabsRoutesMap.get(extractedPathFromUrl)}
</>

Solution

  • If you want to make it with createBrowserRouter you can do it with outlet as right component and left component is normal.

    const Layout = ({ Component }) => (
      <div style={{ display: "flex" }}>
        <div className="box">
          <h2>Left Section</h2>
          <Component />
        </div>
        <div className="box">
          <h2>Right Section</h2>
          <Outlet context="right" />
        </div>
      </div>
    );
    
    const children = () => {
      return [
        { path: "1", element: <Component1 /> },
        { path: "2", element: <Component2 /> },
        { path: "3", element: <Component3 /> },
      ];
    };
    
    const router = createBrowserRouter([
      {
        path: "/",
        element: <RoutesExample />,
        children: [
          {
            path: "1",
            element: <Layout Component={Component1} />,
            children: children(),
          },
          {
            path: "2",
            element: <Layout Component={Component2} />,
            children: children(),
          },
          {
            path: "3",
            element: <Layout Component={Component3} />,
            children: children(),
          },
        ],
      },
    ]);
    

    Demo

    and if you want to use url params here is a simple solution

    const Table = () => {
      const [searchParams, setSearchParams] = useSearchParams();
      const left = searchParams.get("left");
      const right = searchParams.get("right");
      ComponentLeft = componentList[left];
      ComponentRight = componentList[right];
      return left && right ? (
        <div style={{ display: "flex" }}>
          <div className="box">
            <h2>Left Section</h2>
            <ComponentLeft />
          </div>
          <div className="box">
            <h2>Right Section</h2>
            <ComponentRight />
          </div>
        </div>
      ) : (
        <div></div>
      );
    };
    

    where

    const Component1 = () => {
      return <div>1</div>;
    };
    const Component2 = () => {
      return <div>2</div>;
    };
    const Component3 = () => {
      return <div>3</div>;
    };
    
    const componentList = {
      1: Component1,
      2: Component2,
      3: Component3,
    };
    

    Demo