# React Component Architecture (Concept) This document defines the clean concept for React component architecture in `apps/website`. ## Core Principle **Separation of concerns by responsibility, not just by file location.** ## The Four Layers ### 1. App Layer (`app/`) **Purpose**: Entry points and data orchestration **What lives here**: - `page.tsx` - Server Components that fetch data - `layout.tsx` - Root layouts - `route.tsx` - API routes - `*PageClient.tsx` - Client entry points that wire server data to client templates **Rules**: - `page.tsx` does ONLY data fetching and passes raw data to client components - `*PageClient.tsx` manages client state and event handlers - No UI rendering logic (except loading/error states) **Example**: ```typescript // app/teams/page.tsx export default async function TeamsPage() { const query = new TeamsPageQuery(); const result = await query.execute(); if (result.isErr()) { return ; } return ; } // app/teams/TeamsPageClient.tsx 'use client'; export function TeamsPageClient({ teams }: TeamsViewData) { const [searchQuery, setSearchQuery] = useState(''); const router = useRouter(); const handleTeamClick = (teamId: string) => { router.push(`/teams/${teamId}`); }; return ( ); } ``` ### 2. Template Layer (`templates/`) **Purpose**: Composition and layout of components **What lives here**: - Stateless component compositions - Page-level layouts - Component orchestration **Rules**: - Templates ARE stateless (no `useState`, `useEffect`) - Templates CAN use `'use client'` for event handling and composition - Templates receive ViewData (primitives) and event handlers - Templates compose components and UI elements - No business logic - No data fetching **Example**: ```typescript // templates/TeamsTemplate.tsx 'use client'; export function TeamsTemplate({ teams, searchQuery, onSearchChange, onTeamClick }: TeamsTemplateProps) { return (
); } ``` ### 3. Component Layer (`components/`) **Purpose**: Reusable app-specific components **What lives here**: - Components that understand app concepts (teams, races, leagues) - Components that may contain state and business logic - Components that orchestrate UI elements for app purposes **Rules**: - Can be stateful - Can contain app-specific business logic - Can use Next.js hooks (but not Next.js components like `Link`) - Should be reusable within the app context - **Strictly Forbidden**: Generic UI primitives (`Box`, `Surface`) and generic wrappers (`Layout`, `Container`) - **Strictly Forbidden**: Raw HTML tags (use `ui/` components instead) - **Strictly Forbidden**: The `className` or `style` props - **Strictly Forbidden**: Passing implementation details (Tailwind classes, CSS values) as props to UI components **Example**: ```typescript // components/teams/TeamLeaderboardPreview.tsx 'use client'; import { Card, CardHeader, TeamRow } from '@/ui'; export function TeamLeaderboardPreview({ teams, onTeamClick }: Props) { // App-specific logic: medal colors, ranking, etc. const getMedalColor = (position: number) => { if (position === 0) return 'gold'; if (position === 1) return 'silver'; return 'none'; }; return ( {teams.map((team, index) => ( onTeamClick(team.id)} achievement={getMedalColor(index)} /> ))} ); } ``` ### 4. UI Layer (`ui/`) **Purpose**: Generic, reusable UI primitives **What lives here**: - Pure UI elements with no app knowledge - Generic building blocks - Framework-agnostic components **Rules**: - Stateless (no `useState`, `useEffect`) - No app-specific logic - No Next.js imports - Only receive props and render - Maximum reusability - **Encapsulation**: Must NOT expose `className`, `style`, or Tailwind-specific props to consumers. - **Semantic APIs**: Use semantic props (e.g., `variant="primary"`, `size="large"`) instead of implementation details. **Example**: ```typescript // ui/Button.tsx // GOOD: Semantic API export function Button({ children, variant = 'primary', onClick }: ButtonProps) { const classes = { primary: 'bg-blue-600 text-white hover:bg-blue-700', secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300', }[variant]; return ( ); } // BAD: Exposing implementation details // export function Button({ className, backgroundColor, padding }: Props) { ... } ``` ## UI Layering & Primitives To prevent "div wrapper" abuse and maintain architectural integrity, we enforce a strict boundary between **Primitives** and **Semantic UI**. ### Semantic UI & Layout (Allowed in Components) - **Building blocks**: `Card`, `Panel`, `Button`, `Table`. - **Layout Components**: `Layout`, `Container` (in `ui/`). - **Restricted flexibility**: Semantic components are restricted to their defined props. They do NOT allow arbitrary styling props (bg, border, etc.). - **Public API**: These are the only UI elements that should be imported by `components/` or `pages/`. **Rule of thumb**: If you need a styled container or layout in a component, use `Panel`, `Card`, `Layout`, or `Container`. If you need a new type of semantic layout, create it in `ui/` using primitives. Direct use of primitives (`Box`, `Surface`, `Stack`, `Grid`) or raw HTML tags in `components/` is forbidden. ## Clean Component APIs (Anti-Pattern: Prop Pollution) We strictly forbid "Prop Pollution" where implementation details leak into component APIs. This is unmaintainable and unreadable. ### The Problem When a component exposes props like `className`, `style`, `mt={4}`, or `bg="blue-500"`, it: 1. **Breaks Encapsulation**: The consumer needs to know about the internal styling system (Tailwind, CSS). 2. **Increases Fragility**: Changing the internal implementation (e.g., switching from Tailwind to CSS Modules) requires updating all consumers. 3. **Reduces Readability**: Component usage becomes cluttered with styling logic instead of business intent. ### The Solution: Semantic Props Components must only expose props that describe **what** the component is or **how** it should behave semantically, not how it should look in terms of CSS. | Bad Prop (Implementation) | Good Prop (Semantic) | | :--- | :--- | | `className="mt-4 flex items-center"` | `spacing="large"` or `layout="horizontal"` | | `color="#FF0000"` or `color="red-500"` | `intent="danger"` or `variant="error"` | | `style={{ fontWeight: 'bold' }}` | `emphasis="high"` | | `isFullWidth={true}` | `size="full"` | ### Enforcement - **`ui/` components**: May use Tailwind/CSS internally but **MUST NOT** expose these details in their props. - **`components/` components**: **MUST NOT** use `className`, `style`, or any prop that accepts raw styling values. They must only use the semantic APIs provided by `ui/`. - **`templates/`**: Same as `components/`. ## The Formatter & Display Object Layer **Purpose**: Reusable formatting and presentation logic **What lives here**: - `lib/display-objects/` - Deterministic formatting functions - Code-to-label mappings - Value transformations for display **Rules**: - **Formatters**: Stateless utilities for server-side primitive output. - **Display Objects**: Rich Value Objects for client-side interactive APIs. - Class-based - Immutable - Deterministic - No side effects - No `Intl.*` or `toLocale*` (unless client-only) **Usage**: ```typescript // lib/display-objects/RatingDisplay.ts export class RatingDisplay { static format(rating: number): string { return rating.toFixed(0); } static getColor(rating: number): string { if (rating >= 90) return 'text-green'; if (rating >= 70) return 'text-yellow'; return 'text-red'; } } // In ViewData Builder (Server) const viewData = { rating: RatingDisplay.format(dto.rating), // Primitive string }; // In ViewModel (Client) get rating() { return new RatingDisplay(this.data.rating); // Rich API } ``` ## Dependency Flow ``` app/ (page.tsx) ↓ fetches data app/ (*PageClient.tsx) ↓ manages state, creates handlers templates/ (composition) ↓ uses components components/ (app-specific) ↓ uses UI elements ui/ (generic primitives) ``` ## Decision Rules ### When to use *PageClient.tsx? - When you need client-side state management - When you need event handlers that use router/other hooks - When server component needs to be split from client template ### When to use Template vs Component? - **Template**: Page-level composition, orchestrates multiple components - **Component**: Reusable app-specific element with logic ### When to use Component vs UI? - **Component**: Understands app concepts (teams, races, leagues) - **UI**: Generic building blocks (button, card, input) ### When to use Formatters/Display Objects? - When formatting is reusable across multiple ViewModels - When mapping codes to labels - When presentation logic needs to be deterministic ## Key Benefits 1. **Clear boundaries**: Each layer has a single responsibility. 2. **Encapsulation**: Implementation details (HTML, CSS, Tailwind) are hidden within the `ui/` layer. 3. **Semantic APIs**: Components are easier to read and use because they speak the language of the domain, not CSS. 4. **Maintainability**: Changes to styling or internal implementation don't ripple through the entire codebase. 5. **Testability**: Pure functions at UI/Display layer. 6. **Type safety**: Clear, restricted contracts between layers prevent "prop soup".