Files
gh-buzzdan-ai-coding-rules-…/skills/pre-commit-review/reference.md
2025-11-29 18:02:45 +08:00

611 lines
12 KiB
Markdown

# Pre-Commit Review Reference (React/TypeScript)
## Review Philosophy
**Advisory, not blocking**: Inform decisions, don't prevent commits.
**Debt-based categories**: Focus on future maintainability cost.
**Context-aware**: Review changes plus broader file context.
## Complete Review Checklist
### 1. Architecture Patterns
#### ✅ Feature-Based Structure
```
src/features/auth/
├── components/ # Auth UI components
├── hooks/ # Auth custom hooks
├── context/ # Auth context
├── types.ts # Auth types
└── index.ts # Public API
```
#### 🔴 Technical Layer Structure (Design Debt)
```
src/
├── components/auth.tsx
├── hooks/auth.ts
├── contexts/auth.tsx
└── types/auth.ts
```
**Impact**: Features spread across directories, hard to find all related code.
### 2. Primitive Obsession
#### String Primitives
**🔴 Design Debt**:
```typescript
interface User {
id: string // Empty? Invalid format?
email: string // Validated? Format?
phone: string // Format? Country code?
}
function getUser(id: string): User // Any string accepted
```
**✅ Better**:
```typescript
// Branded types
type UserId = Brand<string, 'UserId'>
type Email = Brand<string, 'Email'>
// Or Zod schemas
const UserIdSchema = z.string().uuid()
const EmailSchema = z.string().email()
type UserId = z.infer<typeof UserIdSchema>
type Email = z.infer<typeof EmailSchema>
interface User {
id: UserId
email: Email
phone: PhoneNumber
}
function getUser(id: UserId): User // Only valid IDs accepted
```
#### Number Primitives
**🔴 Design Debt**:
```typescript
interface Product {
price: number // Negative? Too large?
quantity: number // Negative? Zero?
rating: number // Range? Decimal places?
}
```
**✅ Better**:
```typescript
const PriceSchema = z.number().positive().max(1000000)
const QuantitySchema = z.number().int().nonnegative()
const RatingSchema = z.number().min(0).max(5)
type Price = z.infer<typeof PriceSchema>
type Quantity = z.infer<typeof QuantitySchema>
type Rating = z.infer<typeof RatingSchema>
```
#### Boolean State Machines
**🟡 Readability Debt**:
```typescript
const [isLoading, setIsLoading] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const [isError, setIsError] = useState(false)
// Can have invalid states: isLoading && isSuccess
```
**✅ Better**: Discriminated union
```typescript
type State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
// Impossible states are impossible
```
### 3. Component Design
#### Prop Drilling
**🔴 Design Debt**: State passed through 3+ levels
```typescript
<GrandParent user={user}>
<Parent user={user} onUpdate={onUpdate}>
<Child user={user} onUpdate={onUpdate}>
<GrandChild user={user} onUpdate={onUpdate} />
</Child>
</Parent>
</GrandParent>
```
**✅ Fix options**:
1. Context for truly global state
2. Composition to avoid passing props
3. Accept prop drilling for 1-2 levels (it's fine!)
#### Mixed Concerns
**🟡 Readability Debt**: UI + business logic mixed
```typescript
function UserProfile() {
// 50 lines of data fetching
// 30 lines of validation
// 40 lines of state management
// 80 lines of JSX
// Total: 200 lines, hard to test
}
```
**✅ Better**: Separated concerns
```typescript
// testable business logic
function useUserProfile(userId) {
// fetch, validate, manage state
return { user, isLoading, error, actions }
}
// testable UI
function UserProfile({ userId }) {
const { user, isLoading, error, actions } = useUserProfile(userId)
if (isLoading) return <Spinner />
if (error) return <ErrorDisplay error={error} />
return <UserDisplay user={user} actions={actions} />
}
```
#### God Components
**🔴 Design Debt**: Component doing too much (>200 lines)
**Signs**:
- Multiple state variables (5+)
- Many useEffect hooks
- Complex conditional rendering
- Mixed abstraction levels
**Fix**: Extract components and hooks
### 4. Hook Design
#### Hook Extraction
**🟡 Readability Debt**: Logic that should be a hook
```typescript
function Component() {
// 50 lines of reusable logic
// directly in component
}
```
**✅ Better**:
```typescript
function useFeature() {
// extracted, testable, reusable
}
function Component() {
const feature = useFeature()
return <UI feature={feature} />
}
```
#### Hook Dependencies
**🟡 Readability Debt**: Complex dependencies
```typescript
useEffect(() => {
fetchData(id, filters.category, filters.price, sort, page)
}, [id, filters, filters.category, filters.price, sort, page]) // Redundant, complex
```
**✅ Better**:
```typescript
const params = useMemo(
() => ({ id, category: filters.category, price: filters.price, sort, page }),
[id, filters.category, filters.price, sort, page]
)
useEffect(() => {
fetchData(params)
}, [params])
// Or extract to custom hook
function useData(id, filters, sort, page) {
useEffect(() => {
fetchData(id, filters, sort, page)
}, [id, filters, sort, page])
}
```
### 5. Accessibility Review
#### Semantic HTML
| ❌ Don't Use | ✅ Use Instead | Why |
|-------------|--------------|-----|
| `<div onClick>` | `<button>` | Keyboard accessible, screen reader friendly |
| `<div>` for text | `<p>`, `<span>` | Proper semantics |
| `<div>` for navigation | `<nav>` | Landmark for screen readers |
| `<div>` for lists | `<ul>`, `<ol>` | Proper list semantics |
| `<div>` for headings | `<h1>`-`<h6>` | Document outline |
#### Form Accessibility
**🔴 Design Debt**: Missing labels
```typescript
<input type="text" placeholder="Email" />
<input type="password" placeholder="Password" />
```
**✅ Better**:
```typescript
<label htmlFor="email">Email</label>
<input id="email" type="text" />
<label htmlFor="password">Password</label>
<input id="password" type="password" />
```
**🟢 Polish**: Enhanced with descriptions
```typescript
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-describedby="email-hint"
aria-required="true"
/>
<span id="email-hint">We'll never share your email.</span>
```
#### Interactive Elements
**🔴 Design Debt**: Non-semantic interactive elements
```typescript
<div onClick={handleClick}>
Click me
</div>
```
**✅ Better**: Semantic button
```typescript
<button onClick={handleClick}>
Click me
</button>
```
**If div required**:
```typescript
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick()
}
}}
>
Click me
</div>
```
#### Images and Media
**🔴 Design Debt**: Missing alt text
```typescript
<img src="avatar.jpg" />
```
**🟢 Polish**: Generic alt text
```typescript
<img src="avatar.jpg" alt="avatar" />
```
**✅ Better**: Descriptive alt text
```typescript
<img src="avatar.jpg" alt="John Doe's profile picture" />
```
**Decorative images**:
```typescript
<img src="decoration.svg" alt="" role="presentation" />
```
#### Dynamic Content
**🟢 Polish**: Loading without announcement
```typescript
{isLoading && <Spinner />}
```
**✅ Better**: Announced loading
```typescript
{isLoading && (
<div role="status" aria-live="polite">
<span className="sr-only">Loading user data...</span>
<Spinner aria-hidden="true" />
</div>
)}
```
**Error announcements**:
```typescript
{error && (
<div role="alert" aria-live="assertive">
{error.message}
</div>
)}
```
#### Modal Accessibility
**🔴 Design Debt**: Basic modal
```typescript
{isOpen && (
<div className="modal">
<div className="content">
<h2>Title</h2>
<p>Content</p>
<button onClick={onClose}>Close</button>
</div>
</div>
)}
```
**✅ Better**: Accessible modal
```typescript
{isOpen && (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
onKeyDown={(e) => e.key === 'Escape' && onClose()}
>
<div className="content">
<h2 id="modal-title">Title</h2>
<p>Content</p>
<button onClick={onClose} aria-label="Close dialog">
Close
</button>
</div>
</div>
)}
```
**With focus management**:
```typescript
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (isOpen && modalRef.current) {
const previousActiveElement = document.activeElement
modalRef.current.focus()
return () => {
previousActiveElement?.focus()
}
}
}, [isOpen])
// ... rest of modal
}
```
#### Color and Contrast
**🟡 Readability Debt**: Color-only indicators
```typescript
<span style={{ color: 'red' }}>Error</span>
<span style={{ color: 'green' }}>Success</span>
```
**✅ Better**: Multiple indicators
```typescript
<span style={{ color: 'red' }}>
<ErrorIcon aria-hidden="true" />
<span>Error</span>
</span>
```
**With ARIA**:
```typescript
<span
style={{ color: 'red' }}
role="alert"
aria-label="Error"
>
<ErrorIcon aria-hidden="true" />
<span>Invalid email address</span>
</span>
```
### 6. TypeScript Usage
#### Type Safety
**🔴 Design Debt**: Using `any`
```typescript
function processData(data: any) {
// No type safety
}
```
**✅ Better**: Proper types or `unknown` with validation
```typescript
const DataSchema = z.object({
id: z.string(),
name: z.string()
})
function processData(data: unknown) {
const validated = DataSchema.parse(data) // Throws on invalid
// validated is now typed
}
```
#### Props Interfaces
**🟡 Readability Debt**: Inline props type
```typescript
function Button(props: {
label: string
onClick: () => void
variant?: 'primary' | 'secondary'
}) {
// ...
}
```
**✅ Better**: Named interface
```typescript
interface ButtonProps {
label: string
onClick: () => void
variant?: 'primary' | 'secondary'
}
function Button({ label, onClick, variant = 'primary' }: ButtonProps) {
// ...
}
```
#### Discriminated Unions
**🟡 Readability Debt**: Multiple booleans for state
```typescript
interface State {
isLoading: boolean
isSuccess: boolean
isError: boolean
data?: Data
error?: Error
}
```
**✅ Better**: Discriminated union
```typescript
type State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: Data }
| { status: 'error'; error: Error }
// Type narrowing works automatically
if (state.status === 'success') {
state.data // Available, typed correctly
}
```
### 7. Testing Implications
#### Testability
**🔴 Design Debt**: Untestable component
```typescript
function Component() {
// 200 lines of tightly coupled logic
// Can't test without implementation details
}
```
**✅ Better**: Separated, testable
```typescript
// testable hook
function useLogic() {
return { state, actions }
}
// testable component
function Component() {
const { state, actions } = useLogic()
return <UI state={state} actions={actions} />
}
```
### 8. Error Handling
#### Missing Error Boundaries
**🔴 Design Debt**: No error handling
```typescript
function AsyncComponent() {
const data = useAsyncData() // Can throw
return <Display data={data} />
}
```
**✅ Better**: Error boundary wrapper
```typescript
<ErrorBoundary fallback={<ErrorDisplay />}>
<AsyncComponent />
</ErrorBoundary>
```
**Or component-level handling**:
```typescript
function AsyncComponent() {
const { data, error, isLoading } = useAsyncData()
if (isLoading) return <Spinner />
if (error) return <ErrorDisplay error={error} />
if (!data) return <NotFound />
return <Display data={data} />
}
```
## Review Priority
### Must Review (🔴 Design Debt)
1. Primitive obsession
2. Prop drilling (3+ levels)
3. Missing error boundaries
4. Non-semantic interactive elements
5. Missing form labels
6. Using `any` without validation
### Should Review (🟡 Readability Debt)
1. Mixed abstractions
2. Complex conditions
3. God components (>200 lines)
4. Missing hook extraction
5. Inline complex logic
### Nice to Review (🟢 Polish)
1. Missing JSDoc
2. Accessibility enhancements
3. Type improvements
4. Performance optimizations
## Advisory Stance
**Remember**: This is advisory, not blocking.
**User decides**:
- Accept debt (with awareness)
- Fix critical (design debt)
- Fix all
- Expand scope
**Always acknowledge**:
- Time constraints are real
- Team decisions are valid
- Consistency matters
- Sometimes "good enough" is right choice
**Provide options, not mandates**.