| 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 {
: ;
: ;
}
() {
}
() {
}
() {
}
() {
}
{
: ;
}
() {
query = ((tmdbId));
(
);
}
{ };
{ };
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.