12 KiB
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:
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:
// 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:
interface Product {
price: number // Negative? Too large?
quantity: number // Negative? Zero?
rating: number // Range? Decimal places?
}
✅ Better:
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:
const [isLoading, setIsLoading] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const [isError, setIsError] = useState(false)
// Can have invalid states: isLoading && isSuccess
✅ Better: Discriminated union
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
<GrandParent user={user}>
<Parent user={user} onUpdate={onUpdate}>
<Child user={user} onUpdate={onUpdate}>
<GrandChild user={user} onUpdate={onUpdate} />
</Child>
</Parent>
</GrandParent>
✅ Fix options:
- Context for truly global state
- Composition to avoid passing props
- Accept prop drilling for 1-2 levels (it's fine!)
Mixed Concerns
🟡 Readability Debt: UI + business logic mixed
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
// 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
function Component() {
// 50 lines of reusable logic
// directly in component
}
✅ Better:
function useFeature() {
// extracted, testable, reusable
}
function Component() {
const feature = useFeature()
return <UI feature={feature} />
}
Hook Dependencies
🟡 Readability Debt: Complex dependencies
useEffect(() => {
fetchData(id, filters.category, filters.price, sort, page)
}, [id, filters, filters.category, filters.price, sort, page]) // Redundant, complex
✅ Better:
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
<input type="text" placeholder="Email" />
<input type="password" placeholder="Password" />
✅ Better:
<label htmlFor="email">Email</label>
<input id="email" type="text" />
<label htmlFor="password">Password</label>
<input id="password" type="password" />
🟢 Polish: Enhanced with descriptions
<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
<div onClick={handleClick}>
Click me
</div>
✅ Better: Semantic button
<button onClick={handleClick}>
Click me
</button>
If div required:
<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
<img src="avatar.jpg" />
🟢 Polish: Generic alt text
<img src="avatar.jpg" alt="avatar" />
✅ Better: Descriptive alt text
<img src="avatar.jpg" alt="John Doe's profile picture" />
Decorative images:
<img src="decoration.svg" alt="" role="presentation" />
Dynamic Content
🟢 Polish: Loading without announcement
{isLoading && <Spinner />}
✅ Better: Announced loading
{isLoading && (
<div role="status" aria-live="polite">
<span className="sr-only">Loading user data...</span>
<Spinner aria-hidden="true" />
</div>
)}
Error announcements:
{error && (
<div role="alert" aria-live="assertive">
{error.message}
</div>
)}
Modal Accessibility
🔴 Design Debt: Basic modal
{isOpen && (
<div className="modal">
<div className="content">
<h2>Title</h2>
<p>Content</p>
<button onClick={onClose}>Close</button>
</div>
</div>
)}
✅ Better: Accessible modal
{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:
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
<span style={{ color: 'red' }}>Error</span>
<span style={{ color: 'green' }}>Success</span>
✅ Better: Multiple indicators
<span style={{ color: 'red' }}>
<ErrorIcon aria-hidden="true" />
<span>Error</span>
</span>
With ARIA:
<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
function processData(data: any) {
// No type safety
}
✅ Better: Proper types or unknown with validation
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
function Button(props: {
label: string
onClick: () => void
variant?: 'primary' | 'secondary'
}) {
// ...
}
✅ Better: Named interface
interface ButtonProps {
label: string
onClick: () => void
variant?: 'primary' | 'secondary'
}
function Button({ label, onClick, variant = 'primary' }: ButtonProps) {
// ...
}
Discriminated Unions
🟡 Readability Debt: Multiple booleans for state
interface State {
isLoading: boolean
isSuccess: boolean
isError: boolean
data?: Data
error?: Error
}
✅ Better: Discriminated union
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
function Component() {
// 200 lines of tightly coupled logic
// Can't test without implementation details
}
✅ Better: Separated, testable
// 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
function AsyncComponent() {
const data = useAsyncData() // Can throw
return <Display data={data} />
}
✅ Better: Error boundary wrapper
<ErrorBoundary fallback={<ErrorDisplay />}>
<AsyncComponent />
</ErrorBoundary>
Or component-level handling:
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)
- Primitive obsession
- Prop drilling (3+ levels)
- Missing error boundaries
- Non-semantic interactive elements
- Missing form labels
- Using
anywithout validation
Should Review (🟡 Readability Debt)
- Mixed abstractions
- Complex conditions
- God components (>200 lines)
- Missing hook extraction
- Inline complex logic
Nice to Review (🟢 Polish)
- Missing JSDoc
- Accessibility enhancements
- Type improvements
- 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.