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?
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>
);
}