2

I'm trying to create a reusable Tabs component with headless ui (https://headlessui.com/react/tabs) that works with react router

Here is a minimal reproducible example, clicking on a tab takes you to a new page when it should be changing the tab panel and keeping the actual tabs visible on the page, what am I missing?: https://codesandbox.io/s/3ogj23?file=/src/Components/TestComponents/TestComponents.tsx (edit - question resolved, thank you Drew Reese, working sandbox here)

This is the reusable component so far:

export interface TabData {
  route: string;
  label: string;
  component: React.ReactElement;
}    
  
export interface TabProps {
  tabInfo: TabData[];
  onTabChange: (selectedIndex: number) => void;
}
    
    function Tabs({ tabInfo, onTabChange }: TabProps) {
      const [selectedIndex, setSelectedIndex] = useState<number>(0);
    
      return (
        <div>
          <Tab.Group
            selectedIndex={selectedIndex}
            onChange={(selectedTabIndex: number) => {
              setSelectedIndex(selectedTabIndex);
              onTabChange(selectedTabIndex);
            }}
          >
            <Tab.List
              ---styling removed----
            >
              {tabInfo.map((item, index) => (
                <Tab
                  key={index}
                  aria-label={`${item.label}`}
                  name={`${item.label}`}
                  ---styling removed----
                >
                  {item.label}
                </Tab>
              ))}
            </Tab.List>
    
            <Tab.Panels>
              {tabInfo.map((item, index) => (
                <Tab.Panel key={index} className="bg-white p-4">
                  <div>{item.component}</div>
                </Tab.Panel>
              ))}
            </Tab.Panels>
          </Tab.Group>
        </div>
      );
    }
    
    export default Tabs;

And this is a snippet where I am implementing it in another component:

  import { useNavigate } from 'react-router-dom';     
    
    ----other code-------
    
    const navigate = useNavigate();
    
     return (
    
    ----other code-------
    
        const handleOnTabChange = (selectedIndex: number) => {
                
                const path = TabsData[selectedIndex].route;
            
                navigate(`/${path}`, {
                  // not sure this is needed
                  state: { tab: selectedIndex },
                  replace: false,
                });
              };
            
            
        ---irrelevent code removed---
                
           <Tabs tabInfo={TabsData} onTabChange={(selectedIndex) => handleOnTabChange(selectedIndex)} marginWidth={45} />

So basically, the reusable component returns the index of the selected tab so I can use that info to select the route I want from a an array (provided on implementation) like this:

export const TabsData = [
  {
    route: '',
    label: 'Tab 1',
    component: <Component1 />,
  },
  {
    route: 'tab-2-route',
    label: 'Tab 2',
    component: <Component2 />,
  },
  {
    route: 'tab-3-route',
    label: 'Tab 3',
    component: <Component3 />,
  },
  {
    route: 'tab-4-route',
    label: 'Tab 4',
    component: <Component4 />,
  },
];

This works in so far as I get the required path however, when i click on each individual tab I'm taken to the required component in a new browser window not the tab panel

How do I amend my reusable component or my implementation so that when I click on the required tab I get the component in the tab panel rather than a new page?

Code User
  • 25
  • 1
  • 8
  • Can you clarify what you mean by "new page"? Are you saying a new browser window/tab is opened? Can you [edit] the post to clarify and include a more complete [mcve] for where you are rendering the routes and routed components? – Drew Reese Jan 19 '23 at 17:36
  • Yes, the tab component I want appears in a new browser window rather than the tab panel which is where I need it to appear – Code User Jan 19 '23 at 17:38
  • RRD `Link` component shouldn't do that. Think you could create a *running* [codesandbox](https://codesandbox.io/) demo that reproduces this behavior that we could inspect live? – Drew Reese Jan 19 '23 at 17:39
  • For reference i've used `const navigate=useNavigate()` higher up in the component, was that the wrong choice? I'll have a go at a codesandbox, bear with me – Code User Jan 19 '23 at 17:55
  • I need to keep the tabs implementation component on the page but the tab panel to change – Code User Jan 19 '23 at 19:58
  • the fact that I only get the component and the tabs disappear seems to be indicating it's going to another browser window? – Code User Jan 19 '23 at 20:04
  • Ok, this one works (I was hitting 'save' rather than 'save all'!): https://codesandbox.io/s/3ogj23?file=/src/Components/TestComponents/TestComponents.tsx, it clearly shows its finding the routes but its opening a new browser window rather than staying on the tabs page – Code User Jan 19 '23 at 21:11

1 Answers1

3

The TabsImplementation should be converted to a layout route such that it's wrapping the routes/tabs that it controls. As a layout route it renders an Outlet for nested routes to render their content into.

Update the tab data to be more route friendly with element and path properties:

import {
  ComponentOne,
  ComponentTwo,
  ComponentThree,
  ComponentFour
} from "../Components/TestComponents/TestComponents";

export const TabsData = [
  {
    path: "component-one-route",
    label: "Tab 1",
    element: <ComponentOne />
  },
  {
    path: "component-two-route",
    label: "Tab 2",
    element: <ComponentTwo />
  },
  {
    path: "component-three-route",
    label: "Tab 3",
    element: <ComponentThree />
  },
  {
    path: "component-four-route",
    label: "Tab 4",
    element: <ComponentFour />
  }
];

TabsComponent - Renders the Outlet component in the Tab.Panel where the nested route that is matched will render its content.

import { Tab } from "@headlessui/react";
import { useState } from "react";
import { Outlet } from "react-router-dom";

export interface TabData {
  path: string;
  label: string;
  element: React.ReactElement;
}

export interface TabProps {
  tabInfo: TabData[];
  onTabChange: (selectedIndex: number) => void;
}

function Tabs({ tabInfo, onTabChange }: TabProps) {
  const [selectedIndex, setSelectedIndex] = useState<number>(0);

  return (
    <div>
      <Tab.Group
        selectedIndex={selectedIndex}
        onChange={(selectedTabIndex: number) => {
          setSelectedIndex(selectedTabIndex);
          onTabChange(selectedTabIndex);
        }}
      >
        <Tab.List>
          {tabInfo.map((item) => (
            <Tab key={item.path} aria-label={item.label} name={item.label}>
              {item.label}
            </Tab>
          ))}
        </Tab.List>

        <Tab.Panels>
          {tabInfo.map((item) => (
            <Tab.Panel key={item.path} className="bg-white p-4">
              <Outlet /> // <-- Outlet is tab panel content, route fills in
            </Tab.Panel>
          ))}
        </Tab.Panels>
      </Tab.Group>
    </div>
  );
}

export default Tabs;

TabsImplementation

import { useNavigate } from "react-router-dom";
import Tabs from "../Components/TabsComponent";
import { TabsData } from "../data/tabsData";

export default function App() {
  const navigate = useNavigate();

  const handleOnTabChange = (selectedIndex: number) => {
    const { path } = TabsData[selectedIndex];

    navigate(path || "/");
  };

  return (
    <Tabs
      tabInfo={TabsData}
      onTabChange={(selectedIndex) => handleOnTabChange(selectedIndex)}
    />
  );
}

App - maps the tabs data to nested routes rendered in the TabsImplementation layout route.

import "./styles.css";
import { Routes, Route, Navigate } from "react-router-dom";
import TabsImplementation from "./pages/TabsImplementation";
import { TabsData } from "./data/tabsData";

export default function App() {
  return (
    <div className="App">
      <h1>Tabs with Routing with headless-ui</h1>

      <Routes>
        <Route element={<TabsImplementation />}>
          {TabsData.map(({ element, path }) => (
            <Route key={path} {...{ element, path }} />
          ))}
          <Route
            path="*"
            element={<Navigate to={TabsData[0].path} replace />}
          />
        </Route>
      </Routes>
    </div>
  );
}

Edit how-to-use-headless-ui-tabs-with-react-router-6

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • I'll know to click 'Save All' rather than 'Save' next time! I responded last night but SE deleted that original comment, glad you saw this one and I have upvoted the answer - Best wishes – Code User Jan 20 '23 at 06:34