Files
gridpilot.gg/docs/architecture/website/REACT_COMPONENT_ARCHITECTURE.md
2026-01-14 11:38:05 +01:00

6.2 KiB

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:

// 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:

// 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
  • Should NOT be generic UI primitives

Example:

// components/teams/TeamLeaderboardPreview.tsx
'use client';

export function TeamLeaderboardPreview({ teams, onTeamClick }: Props) {
  // App-specific logic: medal colors, ranking, etc.
  const getMedalColor = (position: number) => { /* ... */ };
  
  return (
    <Card>
      <CardHeader>Top Teams</CardHeader>
      {teams.map((team, index) => (
        <TeamRow 
          key={team.id}
          team={team}
          position={index}
          onClick={() => onTeamClick(team.id)}
          medalColor={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

Example:

// ui/Button.tsx
export function Button({ children, variant, onClick }: ButtonProps) {
  return (
    <button 
      className={`btn btn-${variant}`}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

// ui/Card.tsx
export function Card({ children, className }: CardProps) {
  return <div className={`card ${className}`}>{children}</div>;
}

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:

// 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