| name | component-architecture |
| description | Use when creating, modifying, or organizing React components. Covers file organization, naming conventions, composition patterns, export rules, prop design, and component splitting strategies specific to this codebase. |
Component Architecture
This skill defines how components are structured, organized, and composed in the homeflix frontend.
File Organization
Route-Level Components
Components for a specific route live in _components/ under that route:
app/(protected)/library/movies/
├── page.tsx # Server component, composition root
└── _components/
├── featured-movie.tsx # Standalone single-file component
├── movies-filter/ # Multi-file component (folder)
│ ├── index.tsx # Main export
│ ├── active-filters.tsx
│ ├── filter-badge.tsx
│ └── filter-popover.tsx
└── movies-grid/
├── index.tsx
├── movie-card.tsx
└── movie-item.tsx
Rules
- Single-file components — Use a flat
.tsx file when the component has no sub-components (e.g., featured-movie.tsx)
- Multi-file components — Use a folder with
index.tsx when the component has private sub-components (e.g., movies-filter/)
_components/ prefix — All route-private components go in _components/
- Shared components — Reusable cross-route components go in
/components/ (e.g., components/media/, components/query/, components/ui/)
Component File Structure
Every component file follows this internal structure with section separators:
'use client';
import { useQuery } from '@tanstack/react-query';
import { Sparkles } from 'lucide-react';
import { type MovieCredits } from '@/api/entities';
import { tmdbCreditsQueryOptions } from '@/options/queries/tmdb';
import { Query } from '@/components/query';
import { Skeleton } from '@/components/ui/skeleton';
import { SectionHeader } from './section-header';
function getInitials(name: string): string {
}
interface CastCardProps {
name: string;
character: string;
}
function CastCard({ name, character }: CastCardProps) {
}
function CastSectionLoading() {
}
function CastSectionError({ error }: { error: Error }) {
}
function CastSectionContent({ credits }: { credits: MovieCredits }) {
}
interface CastSectionProps {
tmdbId: number;
}
function CastSection({ tmdbId }: CastSectionProps) {
const query = useQuery(tmdbCreditsQueryOptions(tmdbId));
return (
<Query
result={query}
callbacks={{
loading: CastSectionLoading,
error: () => null,
success: (credits) => <CastSectionContent credits={credits} />,
}}
/>
);
}
export type { CastSectionProps };
export { CastSection };
Section Order
'use client' directive (if needed)
- Imports (external → api/types → shared components → local components)
// Utilities — Small private helpers
- Sub-components — Private components used only in this file
// Loading — Skeleton/loading state
// Error — Error state (optional, some sections fail silently)
// Success — Content when data is available
// Main — The exported component that wires query → states
Use // ====...==== separators between major sections.
Exports
Critical rule from CLAUDE.md: Never reexport for convenience.
export type { CastSectionProps };
export { CastSection };
- Named exports only (no
export default)
- Type exports separated from value exports
- Only use
export * from './file' in barrel files for utilities/types, not for component convenience re-exports
Prop Design
Interface-first approach
Always define a Props interface (not type) above the component, using function declarations (not arrows):
interface MovieCardProps {
movie: MovieItem;
status: StatusConfig;
}
function MovieCard({ movie, status }: MovieCardProps) {
}
Slot-based composition (over prop explosion)
When a component needs customizable regions, use slots:
interface MediaCardProps {
href: string;
title: string;
status: StatusConfig;
topRightSlot?: ReactNode;
overlaySlot?: ReactNode;
children?: ReactNode;
}
interface MediaCardProps {
showRating: boolean;
ratingPosition: string;
ratingStyle: string;
showGenres: boolean;
genreLimit: number;
}
Generic type constraints for reusable components
interface BaseMediaItem {
id: string | number;
title: string;
year?: number;
posterUrl?: string;
}
function MediaGrid<T extends BaseMediaItem>({
items,
renderCard,
}: {
items: T[];
renderCard: (item: T, index: number) => ReactNode;
}) {
}
Composition Patterns
Specialized wraps Generic
MovieCard (movie-specific props + logic)
→ wraps MediaCard (generic media props + slots)
→ uses shadcn/ui primitives (Badge, AspectRatio, Tooltip)
Each layer adds domain-specific behavior without modifying the generic layer.
Page as pure composition root
Page components (page.tsx) are server components with zero business logic:
export default function MoviesPage() {
return (
<>
<FeaturedMovie />
<section>
<MoviesFilter />
<MoviesGrid />
</section>
</>
);
}
Each child component manages its own data. No props drilling from pages.
Detail page composition
Detail pages parse the route param and pass it to major sections:
export default async function Page({ params }: PageProps) {
const { id } = await params;
const tmdbId = parseInt(id, 10);
if (isNaN(tmdbId) || tmdbId <= 0) notFound();
return (
<>
<MovieHeader tmdbId={tmdbId} />
<MovieStats tmdbId={tmdbId} />
<MovieTabs tmdbId={tmdbId} />
</>
);
}
Conditional composition
Components conditionally render based on data state:
function MovieTabsContent({ tmdbId, inLibrary }: MovieTabsContentProps) {
return inLibrary ? (
<Tabs defaultValue="overview">
<TabsList>...</TabsList>
<TabsContent value="overview"><OverviewTab tmdbId={tmdbId} /></TabsContent>
<TabsContent value="files"><FilesTab tmdbId={tmdbId} /></TabsContent>
{/* ... */}
</Tabs>
) : (
<OverviewTab tmdbId={tmdbId} />
);
}
Component Splitting Decision
When to split into a folder
Split when a component has 2+ private sub-components that are only used by it:
movies-grid/ → index.tsx + movie-card.tsx + movie-item.tsx
movie-header/ → index.tsx + library-status-badge.tsx
When to keep as a single file
Keep as a single file when sub-components are small and tightly coupled:
cast-section.tsx contains CastCard internally (small, only used here)
featured-movie.tsx contains loading/error/success states internally
Rule of thumb
If the sub-component could be useful outside this component → extract to its own file in the same folder. If it's small (<30 lines) and only used here → keep it internal.