Sub-Pages — Tabs Component¶
Part of: Sub-Pages guide See also: Overview | Data Hook
⚠️ MANDATORY: Create This BEFORE Settings¶
The Tabs component (
<Entity>Tabs.tsx) must be created BEFORE any Settings component.The Tabs component is the parent that:
Extracts the entity ID from the URL
Fetches entity data via the data hook
Passes props to the Settings component
Without
<Entity>Tabs.tsx, a<Entity>Settings.tsxfile is useless — it cannot be rendered or receive data.Creation order:
use<Entity>SettingsData.tsx— Data hook (see 02-data-hook.md)
<Entity>Settings.tsx— Settings component (see 04-settings-tab.md)
<Entity>Tabs.tsx— Tabs component (this file)
Prerequisites¶
Creating a Tabs component alone is not enough for navigation to work. You must also:
Register the route in
AppRoutes.tsxEnable
showLink={true}in the main page’sMainTable
See 04-settings-tab.md for the complete checklist.
What is the Tabs Component?¶
The Tabs component (<Entity>Tabs.tsx) is the entry point for every sub-page. When a user clicks a row in a main page table, React Router navigates to this component. It acts as the “shell” that:
Extracts the entity ID from the URL (e.g.,
/dns-zones/example.com.→example.com.)Fetches entity data using a custom data hook
Renders breadcrumb navigation so users know where they are
Orchestrates tab switching between Settings, membership, or custom tabs
Handles loading and error states (spinner while fetching, “Not Found” for missing entities)
Think of it as the “controller” that coordinates data flow between the URL, the API, and the individual tab components.
Component Lifecycle¶
User clicks row in main page
↓
React Router loads <Entity>Tabs
↓
useSafeParams extracts entity ID from URL
↓
Custom data hook fetches entity from API
↓
While loading → show DataSpinner
↓
If not found → show NotFound page
↓
If found → render Tabs with entity data
Required Imports¶
Note: All imports must be absolute (starting with src/). See 00-best-practices.md § Use Absolute Imports.
import React from "react";
import { PageSection, Tabs, Tab, TabTitleText } from "@patternfly/react-core";
import { useNavigate } from "react-router";
import { NotFound } from "src/components/errors/PageErrors";
import { use<Entity>Settings } from "src/hooks/use<Entity>SettingsData";
import TitleLayout from "src/components/layouts/TitleLayout";
import DataSpinner from "src/components/layouts/DataSpinner";
import BreadCrumb, { BreadCrumbItem } from "src/components/layouts/BreadCrumb/BreadCrumb";
import <Entity>Settings from "src/pages/<Entity>/<Entity>Settings";
import { useSafeParams } from "src/utils/paramsUtils";
URL Parameter Extraction¶
Every sub-page needs to know which entity to display. The entity ID comes from the URL path. We use useSafeParams (a type-safe wrapper around React Router’s useParams) to extract it.
type <Entity>Params = { <primaryKey>: string };
const <Entity>Tabs = ({ section }: <Entity>TabsProps) => {
const { <primaryKey> } = useSafeParams<<Entity>Params>(["<primaryKey>"]);
// ...
};
Common parameter names by entity type:
Entity Type |
Parameter |
Example URL |
|---|---|---|
Users |
|
|
Groups/Rules |
|
|
Hosts |
|
|
DNS Zones |
|
|
Services |
|
|
State Management¶
The component maintains several pieces of state:
// Breadcrumb navigation items
const [breadcrumbItems, setBreadcrumbItems] = React.useState<BreadCrumbItem[]>([]);
// Local copy of entity ID (for convenience in navigation)
const [id, setId] = React.useState("");
// All entity data comes from the custom hook
const <entity>SettingsData = use<Entity>SettingsData(<primaryKey>);
The data hook (covered in 02-data-hook.md) handles all API communication and returns the entity data, loading state, and helper functions.
Loading and Error States¶
Always handle these states before rendering the main content:
// Show spinner while data is being fetched
if (<entity>SettingsData.isLoading || !<entity>SettingsData.<entity>) {
return <DataSpinner />;
}
// Show "Not Found" if the entity doesn't exist
if (!<entity>SettingsData.isLoading && Object.keys(<entity>SettingsData.<entity>).length === 0) {
return <NotFound />;
}
JSX Structure¶
The render output has two main sections:
Header area — breadcrumbs and page title
Tabs area — the PatternFly Tabs component with tab content
return (
<>
<PageSection hasBodyWrapper={false}>
<BreadCrumb breadcrumbItems={breadcrumbItems} />
<TitleLayout id={id} preText="<Entity type>:" text={id} headingLevel="h1" />
</PageSection>
<PageSection hasBodyWrapper={false} type="tabs" isFilled>
<Tabs
activeKey={section}
onSelect={handleTabClick}
variant="secondary"
isBox
className="pf-v6-u-ml-lg"
mountOnEnter
unmountOnExit
>
<Tab eventKey={"settings"} title={<TabTitleText>Settings</TabTitleText>}>
<<Entity>Settings
<entity>={<entity>SettingsData.<entity>}
original<Entity>={<entity>SettingsData.original<Entity>}
metadata={<entity>SettingsData.metadata}
on<Entity>Change={<entity>SettingsData.set<Entity>}
onRefresh={<entity>SettingsData.refetch}
isModified={<entity>SettingsData.modified}
modifiedValues={<entity>SettingsData.modifiedValues}
onResetValues={<entity>SettingsData.resetValues}
pathname={pathname}
/>
</Tab>
{/* Additional tabs go here */}
</Tabs>
</PageSection>
</>
);
Key Tabs props:
activeKey={section}— which tab is currently active (from URL)mountOnEnter/unmountOnExit— lazy-load tabs for performancevariant="secondary"/isBox— PatternFly styling
Tab titles: Use simple <TabTitleText> without Badge components. Badges (showing counts) are only used in the inner TabLayout within membership tab content components, not in the outer <Entity>Tabs.tsx.
Single-Tab vs Multi-Tab¶
Single-tab (Settings only): The tab navigation handler only needs to handle one case. See IdpReferencesTabs.tsx.
Multi-tab: Add more <Tab> elements and extend the navigation handler. See DnsZonesTabs.tsx or HostsTabs.tsx.
Examples¶
Pattern |
Example File |
|---|---|
Single-tab (Settings only) |
|
Multi-tab (Settings + Table) |
|
Multi-tab (Settings + Membership) |
|