refactor: Decompose ConfigPage into focused routing and component layers
Split the over-centralized ConfigPage into focused, composable layers: 1. useTabRouter hook: Encapsulates tab state management and URL synchronization - Maintains selected tab and active item (e.g., jail name) - Syncs state to location.state for deep linking and browser history - Supports bookmarkable URLs and back/forward navigation 2. ConfigPageContainer: Orchestrates tab navigation - Manages TabList and routes tab selection events - Conditionally renders tab content panels - Delegates domain-specific logic to tab components 3. ConfigPage: Focused page layout component - Renders page structure (header, title, description) - Delegates tab orchestration to ConfigPageContainer - No routing or tab state logic Benefits: - Page is now 30 lines vs 125 lines (76% reduction) - Tab state management is reusable for other multi-tab pages - Each tab component remains focused on domain-specific UI - Deep linking and browser history work out of the box - Easier to test and maintain Added comprehensive tests: - useTabRouter: 6 tests covering state initialization, tab selection, and deep linking - ConfigPageContainer: 8 tests covering tab rendering and navigation - ConfigPage: 3 tests for page structure Updated Web-Development.md with tab orchestration pattern documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -242,6 +242,78 @@ The distinction between **`pages/`** and **`components/`** is fundamental to the
|
||||
|
||||
**Example:** `BannedIpsSection` lives in `components/jail/` (not `pages/jail/`) because it is a reusable UI section that presents banned IPs. If a future report or dashboard also needed to show banned IPs, the same component could be imported and reused. By contrast, `JailDetailPage.tsx` lives in `pages/` because it is the top-level route component.
|
||||
|
||||
### Tab Orchestration — ConfigPage Example
|
||||
|
||||
When a page contains tab-based navigation (like the configuration page), isolate routing and tab state management into a dedicated **container component** to prevent the page from becoming over-centralized. This pattern applies to any multi-tab page.
|
||||
|
||||
**Architecture:**
|
||||
|
||||
1. **Page component** (`ConfigPage.tsx`) — renders page layout (header, title, description) and delegates tab routing to a container.
|
||||
2. **Container component** (`ConfigPageContainer.tsx`) — orchestrates tab navigation, manages which tab content is visible, and routes tab selection events.
|
||||
3. **Tab router hook** (`useTabRouter.ts`) — encapsulates tab state synchronization with browser history and supports deep linking (e.g., navigating directly to a specific tab with optional active item like a jail name).
|
||||
4. **Tab components** (`JailsTab.tsx`, `FiltersTab.tsx`, etc.) — domain-specific tab content; each is fully self-contained and receives tab-specific props only.
|
||||
|
||||
**Component tree:**
|
||||
```
|
||||
ConfigPage (page layout)
|
||||
└── ConfigPageContainer (tab orchestration)
|
||||
├── useTabRouter (routing logic)
|
||||
├── JailsTab (jail editing UI)
|
||||
├── FiltersTab (filter editing UI)
|
||||
├── ActionsTab (action editing UI)
|
||||
├── ServerTab (server settings UI)
|
||||
└── RegexTesterTab (regex testing UI)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- **Focused pages** — `ConfigPage` renders only layout; routing logic is in the container.
|
||||
- **Reusable routing** — `useTabRouter` can be used by other pages with tab navigation.
|
||||
- **Isolated tabs** — each tab is a focused component; no shared state entanglement.
|
||||
- **Deep linking** — tab state is synchronized to browser history, allowing bookmarkable URLs and the back/forward buttons to work correctly.
|
||||
|
||||
**Key pattern:**
|
||||
|
||||
```tsx
|
||||
// hooks/useTabRouter.ts — routes and state
|
||||
export type ConfigTabId = "jails" | "filters" | "actions" | "server" | "regex";
|
||||
|
||||
export function useTabRouter(): { activeTab: ConfigTabId; selectTab: (tab: ConfigTabId) => void; ... } {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
// Sync tab state to location.state for deep linking
|
||||
// ... (see implementation)
|
||||
}
|
||||
|
||||
// components/config/ConfigPageContainer.tsx — renders tabs
|
||||
export function ConfigPageContainer(): JSX.Element {
|
||||
const { activeTab, selectTab } = useTabRouter();
|
||||
return (
|
||||
<>
|
||||
<TabList selectedValue={activeTab} onTabSelect={...}>
|
||||
{/* Tabs */}
|
||||
</TabList>
|
||||
{/* Tab content panels, conditionally rendered via CSS */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// pages/ConfigPage.tsx — layout only
|
||||
export function ConfigPage(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
{/* Page header, title, description */}
|
||||
<ConfigPageContainer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Avoid:**
|
||||
|
||||
- Mixing page layout, tab orchestration, and tab content in one file.
|
||||
- Duplicating tab state across multiple hooks or components.
|
||||
- Hardcoding tab IDs as strings — use the `ConfigTabId` type.
|
||||
|
||||
### Separation of Concerns
|
||||
|
||||
- **Pages** handle routing and compose layout + components — they contain no business logic.
|
||||
|
||||
Reference in New Issue
Block a user