Patterns
Multi-Tab Detail Pages
Pattern for implementing detail/edit pages with multiple tabs, centralized store management, and URL-persisted tab state.
Architecture Overview
- URL-based tab state - Persists in URL, survives refresh
- Centralized store - Main entity store passed through component tree
- Lazy-loaded tab data - Tabs query their own related data
- Loading gates - Handled at page level, not in tabs
Component Structure
page.tsx (Client Component)
├── Creates main store
├── Queries store on route param change
├── Handles loading/error states
└── Renders PageShell + DetailPage
page-content.tsx (Layout)
├── Receives store + row as props
├── Creates related stores
├── Manages tab state (URL-synced)
└── Renders tabs
tabs/*.tsx (Tab Components)
├── Receive store + row as props
├── Use store for mutations
└── Render tab-specific UI
Page with Store and Loading
// page.tsx
'use client';
import { use, useEffect } from 'react';
import { PageShell } from '@wayvo-ai/core/ui';
import { useIsStoreLoading, useStoreError, useCurrentRow } from '@wayvo-ai/core/ui';
import { Loader2 } from 'lucide-react';
import type { DBRow } from '@wayvo-ai/core/common';
import type { Entity } from '@/lib/common/ds/types/module/Entity';
import { useEntityStore } from './hooks/use-store';
import { DetailPage } from './page-content';
interface PageProps {
params: Promise<{ id: string }>;
}
export default function Page({ params }: PageProps) {
const store = useEntityStore();
const { id } = use(params);
useEffect(() => {
store.executeQuery({
query: { match: { id } },
force: true,
});
}, [id, store]);
const isLoading = useIsStoreLoading(store);
const hasError = useStoreError(store);
const row = useCurrentRow(store) as DBRow<Entity>;
return (
<PageShell title={`Entity: ${id}`} enableShareUrl>
{isLoading || row?.id !== id ? (
<div className="flex h-full items-center justify-center">
<Loader2 className="size-10 animate-spin" />
</div>
) : hasError ? (
<div className="flex h-full items-center justify-center">
<p className="text-destructive">Failed to load. Please try again.</p>
</div>
) : !row ? (
<div className="flex h-full items-center justify-center">
<p>Entity not found</p>
</div>
) : (
<DetailPage entity={row} entityStore={store} />
)}
</PageShell>
);
}
Tab State Management
// page-content.tsx
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import type { Store } from '@wayvo-ai/core/common';
import type { Entity } from '@/lib/common/ds/types/module/Entity';
import { OverviewTab } from './tabs/overview';
import { DetailsTab } from './tabs/details';
export function DetailPage({ entity, entityStore }: { entity: Entity; entityStore: Store<Entity> }) {
const router = useRouter();
const searchParams = useSearchParams();
const activeTab = searchParams.get('tab') || 'overview';
const handleTabChange = (value: string) => {
const params = new URLSearchParams(searchParams.toString());
params.set('tab', value);
router.push(`?${params.toString()}`);
};
return (
<Tabs value={activeTab} onValueChange={handleTabChange}>
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="details">Details</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<OverviewTab entity={entity} entityStore={entityStore} />
</TabsContent>
<TabsContent value="details">
<DetailsTab entity={entity} entityStore={entityStore} />
</TabsContent>
</Tabs>
);
}
Tab Components
// tabs/overview.tsx
'use client';
import type { Store } from '@wayvo-ai/core/common';
import type { Entity } from '@/lib/common/ds/types/module/Entity';
import { TextInput } from '@wayvo-ai/core/ui';
import { useCurrentRowSync } from '@wayvo-ai/core/ui';
export function OverviewTab({ entity, entityStore }: { entity: Entity; entityStore: Store<Entity> }) {
const row = useCurrentRowSync(entityStore);
return (
<div className="grid gap-4 p-4">
<TextInput
label="Name"
value={row?.name || ''}
onChange={(value) => entityStore.setValue('name', value)}
/>
</div>
);
}
Key Principles
- Centralized store - Main entity store passed to all tabs
- URL state - Tab state in URL query params
- Loading at page level - Handle loading/error in page.tsx
- Lazy tab data - Each tab queries its own related data
- Shared mutations - All tabs use the same store for updates
Next Steps
- Page Pattern - Basic page structure
- Stores - Store management