314 lines
10 KiB
Markdown
314 lines
10 KiB
Markdown
# 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 <ErrorTeams />;
|
|
}
|
|
|
|
return <TeamsPageClient teams={result.value.teams} />;
|
|
}
|
|
|
|
// 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 (
|
|
<TeamsTemplate
|
|
teams={teams}
|
|
searchQuery={searchQuery}
|
|
onSearchChange={setSearchQuery}
|
|
onTeamClick={handleTeamClick}
|
|
/>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 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 (
|
|
<main>
|
|
<Header>
|
|
<SearchInput value={searchQuery} onChange={onSearchChange} />
|
|
</Header>
|
|
|
|
<TeamLeaderboardPreview
|
|
teams={teams}
|
|
onTeamClick={onTeamClick}
|
|
/>
|
|
|
|
<TeamGrid teams={teams} onTeamClick={onTeamClick} />
|
|
</main>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 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 (
|
|
<Card variant="elevated">
|
|
<CardHeader title="Top Teams" />
|
|
{teams.map((team, index) => (
|
|
<TeamRow
|
|
key={team.id}
|
|
name={team.name}
|
|
rank={index + 1}
|
|
onClick={() => onTeamClick(team.id)}
|
|
achievement={getMedalColor(index)}
|
|
/>
|
|
))}
|
|
</Card>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 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 (
|
|
<button
|
|
className={`px-4 py-2 rounded-md transition-colors ${classes}`}
|
|
onClick={onClick}
|
|
>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
// 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".
|