diff --git a/apps/website/eslint-rules/template-purity-rules.js b/apps/website/eslint-rules/template-purity-rules.js index 6f43f93f4..1607db5cf 100644 --- a/apps/website/eslint-rules/template-purity-rules.js +++ b/apps/website/eslint-rules/template-purity-rules.js @@ -63,6 +63,38 @@ module.exports = { }, }, + // Rule 8: No 'use client' directive in templates + 'no-use-client-in-templates': { + meta: { + type: 'problem', + docs: { + description: 'Forbid use client directive in templates', + category: 'Template Purity', + }, + messages: { + message: 'Templates must not use "use client" directive - they should be stateless composition', + }, + }, + create(context) { + const filename = context.getFilename(); + const isInTemplates = filename.includes('/templates/'); + + if (!isInTemplates) return {}; + + return { + ExpressionStatement(node) { + if (node.expression.type === 'Literal' && + node.expression.value === 'use client') { + context.report({ + node, + messageId: 'message', + }); + } + }, + }; + }, + }, + // Rule 3: No computations in templates 'no-computations-in-templates': { meta: { diff --git a/apps/website/eslint-rules/ui-element-purity.js b/apps/website/eslint-rules/ui-element-purity.js index df10bec0c..e83a1a7dd 100644 --- a/apps/website/eslint-rules/ui-element-purity.js +++ b/apps/website/eslint-rules/ui-element-purity.js @@ -35,11 +35,18 @@ module.exports = { if (!isInUi) return {}; - let hasStateHooks = false; - let hasEffectHooks = false; - let hasContext = false; - return { + // Check for 'use client' directive + ExpressionStatement(node) { + if (node.expression.type === 'Literal' && + node.expression.value === 'use client') { + context.report({ + node, + messageId: 'noStateInUi', + }); + } + }, + // Check for state hooks CallExpression(node) { if (node.callee.type !== 'Identifier') return; @@ -48,7 +55,6 @@ module.exports = { // State management hooks if (['useState', 'useReducer', 'useRef'].includes(hookName)) { - hasStateHooks = true; context.report({ node, messageId: 'noStateInUi', @@ -57,7 +63,6 @@ module.exports = { // Effect hooks if (['useEffect', 'useLayoutEffect', 'useInsertionEffect'].includes(hookName)) { - hasEffectHooks = true; context.report({ node, messageId: 'noSideEffects', @@ -66,7 +71,6 @@ module.exports = { // Context (can introduce state) if (hookName === 'useContext') { - hasContext = true; context.report({ node, messageId: 'noStateInUi', @@ -76,8 +80,8 @@ module.exports = { // Check for class components with state ClassDeclaration(node) { - if (node.superClass && - node.superClass.type === 'Identifier' && + if (node.superClass && + node.superClass.type === 'Identifier' && node.superClass.name === 'Component') { context.report({ node, @@ -90,7 +94,7 @@ module.exports = { AssignmentExpression(node) { if (node.left.type === 'MemberExpression' && node.left.property.type === 'Identifier' && - (node.left.property.name === 'state' || + (node.left.property.name === 'state' || node.left.property.name === 'setState')) { context.report({ node, diff --git a/docs/architecture/website/REACT_COMPONENT_ARCHITECTURE.md b/docs/architecture/website/REACT_COMPONENT_ARCHITECTURE.md new file mode 100644 index 000000000..6e003ef87 --- /dev/null +++ b/docs/architecture/website/REACT_COMPONENT_ARCHITECTURE.md @@ -0,0 +1,254 @@ +# 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 +- Should NOT be generic UI primitives + +**Example**: +```typescript +// components/teams/TeamLeaderboardPreview.tsx +'use client'; + +export function TeamLeaderboardPreview({ teams, onTeamClick }: Props) { + // App-specific logic: medal colors, ranking, etc. + const getMedalColor = (position: number) => { /* ... */ }; + + return ( + + Top Teams + {teams.map((team, index) => ( + onTeamClick(team.id)} + medalColor={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 + +**Example**: +```typescript +// ui/Button.tsx +export function Button({ children, variant, onClick }: ButtonProps) { + return ( + + ); +} + +// ui/Card.tsx +export function Card({ children, className }: CardProps) { + return
{children}
; +} +``` + +## The 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**: +- Class-based +- Immutable +- Deterministic +- No side effects +- No `Intl.*` or `toLocale*` + +**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 ViewModel Builder +const viewModel = { + rating: RatingDisplay.format(dto.rating), + ratingColor: RatingDisplay.getColor(dto.rating) +}; +``` + +## 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 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. **Testability**: Pure functions at UI/Display layer +3. **Reusability**: UI elements can be used anywhere +4. **Maintainability**: Changes in one layer don't ripple +5. **Type safety**: Clear contracts between layers \ No newline at end of file