400 lines
8.3 KiB
Markdown
400 lines
8.3 KiB
Markdown
# Component Architecture
|
|
|
|
This document defines the strict separation of concerns for all UI code in the website layer.
|
|
|
|
## The Three Layers
|
|
|
|
```
|
|
apps/website/
|
|
├── app/ ← App Layer (Pages & Layouts)
|
|
├── components/ ← Component Layer (App Components)
|
|
├── ui/ ← UI Layer (Pure Elements)
|
|
└── hooks/ ← Shared Logic
|
|
```
|
|
|
|
---
|
|
|
|
## 1. App Layer (`app/`)
|
|
|
|
**Purpose**: Pages, layouts, and routing configuration.
|
|
|
|
**Characteristics**:
|
|
- ✅ Only page.tsx, layout.tsx, route.tsx files
|
|
- ✅ Can import from `components/`, `ui/`, `hooks/`
|
|
- ✅ Can use Next.js features (redirect, cookies, headers)
|
|
- ✅ Can use Server Components
|
|
- ❌ **NO raw HTML with styling**
|
|
- ❌ **NO business logic**
|
|
- ❌ **NO state management**
|
|
|
|
**Allowed**:
|
|
```typescript
|
|
// app/dashboard/page.tsx
|
|
export default function DashboardPage() {
|
|
return (
|
|
<DashboardLayout>
|
|
<DashboardHeader />
|
|
<DashboardStats />
|
|
<DashboardActions />
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
```
|
|
|
|
**Forbidden**:
|
|
```typescript
|
|
// ❌ WRONG - Raw HTML in app/
|
|
export default function DashboardPage() {
|
|
return (
|
|
<div className="bg-white p-6"> {/* ❌ No raw HTML with styling */}
|
|
<h1 className="text-2xl">Dashboard</h1>
|
|
<button onClick={handleSubmit}>Click</button> {/* ❌ No inline handlers */}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Component Layer (`components/`)
|
|
|
|
**Purpose**: App-level components that may contain state and business logic.
|
|
|
|
**Characteristics**:
|
|
- ✅ Can be stateful (useState, useReducer)
|
|
- ✅ Can have side effects (useEffect)
|
|
- ✅ Can use hooks
|
|
- ✅ Can contain business logic
|
|
- ✅ Can import from `ui/`, `hooks/`
|
|
- ❌ **NO Next.js imports** (navigation, routing)
|
|
- ❌ **NO raw HTML with styling** (use ui/ elements)
|
|
|
|
**Allowed**:
|
|
```typescript
|
|
// components/DashboardHeader.tsx
|
|
export function DashboardHeader() {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
return (
|
|
<header>
|
|
<Button onClick={() => setIsOpen(!isOpen)}>Toggle</Button>
|
|
{isOpen && <Menu />}
|
|
</header>
|
|
);
|
|
}
|
|
```
|
|
|
|
**Forbidden**:
|
|
```typescript
|
|
// ❌ WRONG - Next.js imports in components/
|
|
import { useRouter } from 'next/navigation'; // ❌
|
|
|
|
export function DashboardHeader() {
|
|
const router = useRouter(); // ❌
|
|
// ...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 3. UI Layer (`ui/`)
|
|
|
|
**Purpose**: Pure, reusable, stateless UI elements.
|
|
|
|
**Characteristics**:
|
|
- ✅ Stateless (no useState, useReducer)
|
|
- ✅ No side effects (no useEffect)
|
|
- ✅ Pure functions based on props
|
|
- ✅ Maximum reusability
|
|
- ✅ Framework-agnostic
|
|
- ❌ **NO state management**
|
|
- ❌ **NO Next.js imports**
|
|
- ❌ **NO business logic**
|
|
|
|
**Allowed**:
|
|
```typescript
|
|
// ui/Button.tsx
|
|
export function Button({ children, onClick, variant = 'primary' }) {
|
|
const className = variant === 'primary'
|
|
? 'bg-blue-500 text-white'
|
|
: 'bg-gray-200 text-black';
|
|
|
|
return (
|
|
<button className={className} onClick={onClick}>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
// ui/Card.tsx
|
|
export function Card({ children, className = '' }) {
|
|
return (
|
|
<div className={`bg-white rounded-lg shadow ${className}`}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
**Forbidden**:
|
|
```typescript
|
|
// ❌ WRONG - State in UI element
|
|
export function Button({ children }) {
|
|
const [isLoading, setIsLoading] = useState(false); // ❌
|
|
|
|
return <button>{isLoading ? 'Loading...' : children}</button>;
|
|
}
|
|
|
|
// ❌ WRONG - Next.js imports
|
|
import { useRouter } from 'next/navigation'; // ❌
|
|
|
|
export function Link({ href, children }) {
|
|
const router = useRouter(); // ❌
|
|
// ...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Hooks Layer (`hooks/`)
|
|
|
|
**Purpose**: Shared stateful logic.
|
|
|
|
**Characteristics**:
|
|
- ✅ Can use all hooks
|
|
- ✅ Can contain business logic
|
|
- ✅ Can be used by components and pages
|
|
- ❌ **NO JSX**
|
|
- ❌ **NO UI rendering**
|
|
|
|
**Allowed**:
|
|
```typescript
|
|
// hooks/useDropdown.ts
|
|
export function useDropdown() {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
return {
|
|
isOpen,
|
|
open: () => setIsOpen(true),
|
|
close: () => setIsOpen(false),
|
|
toggle: () => setIsOpen(!isOpen),
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## ESLint Enforcement
|
|
|
|
### 1. **No Raw HTML in `app/`**
|
|
```json
|
|
{
|
|
"files": ["app/**/*.tsx", "app/**/*.ts"],
|
|
"rules": {
|
|
"gridpilot-rules/no-raw-html-in-app": "error"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Catches**:
|
|
- `<div className="...">` in app/
|
|
- `<button style={{...}}>` in app/
|
|
- `<span onClick={...}>` in app/
|
|
|
|
### 2. **UI Element Purity**
|
|
```json
|
|
{
|
|
"files": ["ui/**/*.tsx", "ui/**/*.ts"],
|
|
"rules": {
|
|
"gridpilot-rules/ui-element-purity": "error"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Catches**:
|
|
- `useState` in ui/
|
|
- `useEffect` in ui/
|
|
- `useContext` in ui/
|
|
|
|
### 3. **No Next.js in UI/Components**
|
|
```json
|
|
{
|
|
"files": ["ui/**/*", "components/**/*"],
|
|
"rules": {
|
|
"gridpilot-rules/no-nextjs-imports-in-ui": "error"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Catches**:
|
|
- `import { useRouter } from 'next/navigation'`
|
|
- `import { redirect } from 'next/navigation'`
|
|
- `import Link from 'next/link'`
|
|
|
|
### 4. **Component Classification (Suggestions)**
|
|
```json
|
|
{
|
|
"files": ["components/**/*", "ui/**/*"],
|
|
"rules": {
|
|
"gridpilot-rules/component-classification": "warn"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Suggests**:
|
|
- Move pure components to ui/
|
|
- Move stateful elements to components/
|
|
|
|
---
|
|
|
|
## Dependency Flow
|
|
|
|
```
|
|
app/ (pages)
|
|
↓ imports from
|
|
components/ (stateful)
|
|
↓ imports from
|
|
ui/ (pure)
|
|
↓ imports from
|
|
hooks/ (logic)
|
|
```
|
|
|
|
**Never**:
|
|
- app/ → hooks/ (pages don't need hooks directly)
|
|
- ui/ → components/ (ui must stay pure)
|
|
- ui/ → next/navigation (ui must be framework-agnostic)
|
|
- components/ → next/navigation (navigation should be passed from pages)
|
|
|
|
---
|
|
|
|
## Examples
|
|
|
|
### ✅ Correct Architecture
|
|
|
|
```typescript
|
|
// app/dashboard/page.tsx
|
|
export default function DashboardPage() {
|
|
return (
|
|
<DashboardLayout>
|
|
<DashboardHeader />
|
|
<DashboardStats />
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
// components/DashboardHeader.tsx
|
|
export function DashboardHeader() {
|
|
const [search, setSearch] = useState('');
|
|
|
|
return (
|
|
<header>
|
|
<SearchInput value={search} onChange={setSearch} />
|
|
<Button onClick={handleSearch}>Search</Button>
|
|
</header>
|
|
);
|
|
}
|
|
|
|
// ui/SearchInput.tsx
|
|
export function SearchInput({ value, onChange }) {
|
|
return (
|
|
<input
|
|
value={value}
|
|
onChange={e => onChange(e.target.value)}
|
|
className="border p-2"
|
|
/>
|
|
);
|
|
}
|
|
|
|
// ui/Button.tsx
|
|
export function Button({ children, onClick }) {
|
|
return (
|
|
<button onClick={onClick} className="bg-blue-500 text-white">
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|
|
```
|
|
|
|
### ❌ Wrong Architecture
|
|
|
|
```typescript
|
|
// app/dashboard/page.tsx
|
|
export default function DashboardPage() {
|
|
const [search, setSearch] = useState(''); // ❌ State in page
|
|
|
|
return (
|
|
<div className="p-6"> {/* ❌ Raw HTML */}
|
|
<input
|
|
value={search}
|
|
onChange={e => setSearch(e.target.value)} // ❌ State logic
|
|
/>
|
|
<button onClick={() => router.push('/search')}> {/* ❌ Router in page */}
|
|
Search
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Benefits
|
|
|
|
1. **Reusability**: UI elements can be used anywhere
|
|
2. **Testability**: Pure functions are easy to test
|
|
3. **Maintainability**: Clear separation of concerns
|
|
4. **Performance**: No unnecessary re-renders
|
|
5. **Type Safety**: Each layer has clear contracts
|
|
|
|
---
|
|
|
|
## Migration Guide
|
|
|
|
If you have existing code that violates these rules:
|
|
|
|
1. **Extract UI elements** from app/ to ui/
|
|
2. **Move stateful logic** from ui/ to components/
|
|
3. **Remove Next.js imports** from components/ui (pass callbacks from pages)
|
|
4. **Use hooks/** for shared logic
|
|
|
|
**Example migration**:
|
|
```typescript
|
|
// Before
|
|
// app/page.tsx
|
|
export default function Page() {
|
|
return <div className="bg-white p-6">...</div>;
|
|
}
|
|
|
|
// After
|
|
// app/page.tsx
|
|
export default function Page() {
|
|
return <HomePage />;
|
|
}
|
|
|
|
// components/HomePage.tsx
|
|
export function HomePage() {
|
|
return (
|
|
<PageContainer>
|
|
<HomeContent />
|
|
</PageContainer>
|
|
);
|
|
}
|
|
|
|
// ui/PageContainer.tsx
|
|
export function PageContainer({ children }) {
|
|
return <div className="bg-white p-6">{children}</div>;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
| Layer | State | Next.js | HTML | Purpose |
|
|
|-------|-------|---------|------|---------|
|
|
| **app/** | ❌ No | ✅ Yes | ❌ No | Pages & layouts |
|
|
| **components/** | ✅ Yes | ❌ No | ❌ No | App components |
|
|
| **ui/** | ❌ No | ❌ No | ✅ Yes | Pure elements |
|
|
| **hooks/** | ✅ Yes | ❌ No | ❌ No | Shared logic |
|
|
|
|
**Golden Rule**: Each layer should only depend on layers below it. |