Comprehensive React component coding guidelines, refactoring principles, and architectural patterns. **CRITICAL**: Focuses on patterns AI commonly fails to implement correctly, especially testability, props control, and component responsibility separation. Reference this skill when implementing or refactoring React components during Phase 2.
This skill focuses on patterns AI commonly fails to implement correctly. Trust AI for syntax and structure, but scrutinize these critical areas where AI consistently makes mistakes.
Pattern AI ALWAYS gets wrong: Creating components that control UI branches with internal state
// ❌ Typical AI pattern (untestable)
function UserProfile({ userId }: { userId: string }) {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<Error | null>(null)
const [user, setUser] = useState<User | null>(null)
// Problem 1: To test loading state, you must actually trigger fetch to set loading=true
// Problem 2: To test error state, you must make fetch fail
// Problem 3: Cannot test each state independently
if (loading) return <Spinner />
if (error) return <ErrorMessage error={error} />
if (!user) return <div>Not found</div>
return <div>{user.name}</div>
}
Why is this untestable?:
loading, error, user)Correct pattern: Convert internal state to props, separate components by state
// ✅ Testable pattern
type UserProfileProps = {
user: User | null
}
function UserProfile({ user }: UserProfileProps) {
if (!user) return <div>Not found</div>
return <div>{user.name}</div>
}
// Easy to test
test('displays Not found when user is null', () => {
render(<UserProfile user={null} />)
expect(screen.getByText('Not found')).toBeInTheDocument()
})
test('displays user name', () => {
const user = { name: 'Taro', id: '1' }
render(<UserProfile user={user} />)
expect(screen.getByText('Taro')).toBeInTheDocument()
})
Same applies to functions:
// ❌ AI writes: depends on internal state
function validateUser() {
const user = getCurrentUser() // depends on global state
if (!user.email) return false
return true
}
// ✅ Correct: controllable via arguments
function validateUser(user: User): boolean {
if (!user.email) return false
return true
}
Pattern AI ALWAYS gets wrong: Components hold internal state that cannot be controlled externally
// ❌ AI writes: trapped in internal state
function UserCard({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null)
useEffect(() => {
fetchUser(userId).then(setUser)
}, [userId])
// Problem: Cannot control loading/error states from parent
// Problem: Cannot use Suspense
// Problem: Actual fetch runs during tests
return user ? <div>{user.name}</div> : <div>Loading...</div>
}
// Cannot control loading state from parent
<UserCard userId="123" /> // Cannot change Loading display
Correct pattern: Make everything controllable via props
// ✅ Correct: everything controlled via props
type UserCardProps = {
user: User | null
isLoading?: boolean
error?: Error | null
}
function UserCard({ user, isLoading, error }: UserCardProps) {
if (isLoading) return <Spinner />
if (error) return <ErrorMessage error={error} />
if (!user) return <div>Not found</div>
return <div>{user.name}</div>
}
// Fully controllable from parent
function UserPage() {
const { user, isLoading, error } = useUser('123')
return <UserCard user={user} isLoading={isLoading} error={error} />
}
// Easy to test
test('loading state', () => {
render(<UserCard user={null} isLoading={true} />)
expect(screen.getByTestId('spinner')).toBeInTheDocument()
})
Pattern AI ALWAYS gets wrong: Cramming multiple conditional branches into one component
// ❌ AI writes: scattered conditional branches
function Dashboard() {
const { user, subscription, notifications } = useData()
return (
<div>
{/* Problem 1: user conditional branch */}
{user ? (
<div>
<h1>{user.name}</h1>
{/* Problem 2: subscription conditional branch */}
{subscription?.isPremium ? (
<PremiumBadge />
) : (
<FreeBadge />
)}
</div>
) : (
<LoginPrompt />
)}
{/* Problem 3: notifications conditional branch */}
{notifications.length > 0 ? (
<NotificationList items={notifications} />
) : (
<EmptyState />
)}
</div>
)
}
// Problems with this design:
// - Cannot test each conditional branch independently
// - To test PremiumBadge display, need user + subscription.isPremium=true
// - Combination of multiple states = test cases explode
Correct pattern: Separate components for each conditional branch
// ✅ Correct: extract conditional branches into separate components
type UserSectionProps = {
user: User | null
subscription: Subscription | null
}
function UserSection({ user, subscription }: UserSectionProps) {
if (!user) return <LoginPrompt />
return (
<div>
<h1>{user.name}</h1>
<SubscriptionBadge subscription={subscription} />
</div>
)
}
type SubscriptionBadgeProps = {
subscription: Subscription | null
}
function SubscriptionBadge({ subscription }: SubscriptionBadgeProps) {
if (subscription?.isPremium) return <PremiumBadge />
return <FreeBadge />
}
type NotificationSectionProps = {
notifications: Notification[]
}
function NotificationSection({ notifications }: NotificationSectionProps) {
if (notifications.length === 0) return <EmptyState />
return <NotificationList items={notifications} />
}
// Easy to test
test('displays premium badge', () => {
const subscription = { isPremium: true }
render(<SubscriptionBadge subscription={subscription} />)
expect(screen.getByTestId('premium-badge')).toBeInTheDocument()
})
test('displays free badge', () => {
render(<SubscriptionBadge subscription={null} />)
expect(screen.getByTestId('free-badge')).toBeInTheDocument()
})
Pattern AI ALWAYS gets wrong: Data fetching with useEffect
// ❌ AI writes: data fetching with useEffect
'use client'
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data)
setLoading(false)
})
}, [userId])
// Problem 1: Cannot use Suspense
// Problem 2: Cannot SSR (becomes Client Component)
// Problem 3: Cannot control loading state from parent
// Problem 4: Must mock fetch during tests
if (loading) return <Spinner />
return <div>{user?.name}</div>
}
Correct pattern: Data fetching in Server Component, display in Presentational Component
// ✅ Correct: Data fetching in Server Component
async function UserProfileServer({ userId }: { userId: string }) {
const user = await fetchUser(userId) // Direct async/await
return <UserProfile user={user} />
}
// Presentational Component
type UserProfileProps = {
user: User
}
function UserProfile({ user }: UserProfileProps) {
return <div>{user.name}</div>
}
// Usage: Loading management with Suspense
<Suspense fallback={<Spinner />}>
<UserProfileServer userId="123" />
</Suspense>
// Easy to test (data fetching and display separated)
test('displays user name', () => {
const user = { name: 'Taro', id: '1' }
render(<UserProfile user={user} />)
expect(screen.getByText('Taro')).toBeInTheDocument()
})
Eight core principles guide component refactoring:
CRITICAL marked principles are areas where AI ALWAYS makes mistakes.
Directory Naming:
kebab-case matching component namePascalCase file directly inside rootread-only-editor/ReadOnlyEditor.tsxParent-Child Hierarchy:
parent-component/child-component/grandchild-component/Export Strategy:
bun run check:fix and bun run typecheckany - Solve issues fundamentally, document when unavoidable@ts-ignore or // biome-ignore without reasonBefore considering implementation complete, verify AI didn't fall into these traps:
any typesTestable Component Pattern:
// ✅ Props control all states
type ComponentProps = {
data: Data | null
isLoading?: boolean
error?: Error | null
}
function Component({ data, isLoading, error }: ComponentProps) {
if (isLoading) return <Spinner />
if (error) return <ErrorMessage error={error} />
if (!data) return <EmptyState />
return <Content data={data} />
}
Logic Extraction:
// Extract to userValidation.ts
export const validateEmail = (email: string): boolean => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
Presenter Pattern:
// presenter.ts
export const getUserStatusText = (status: string): string => {
switch (status) {
case "active": return "Active"
case "inactive": return "Inactive"
default: return "Unknown"
}
}
This skill is primarily referenced during Phase 2 (Implementation) when:
After implementation, code undergoes review in Phase 2 (Code Review) using Codex MCP.
// Server Component (async)
export async function UserProfileServer({ userId }: { userId: string }) {
const user = await fetchUser(userId) // Direct async/await
return <UserProfile user={user} />
}
// Usage with Suspense
<Suspense fallback={<Spinner />}>
<UserProfileServer userId={userId} />
</Suspense>
// presenter.ts - Pure functions for display logic
export const getUserStatusText = (status: string): string => { /* ... */ }
export const getUserStatusColor = (status: string): string => { /* ... */ }
// Component uses presenter
<Badge color={getUserStatusColor(status)}>
{getUserStatusText(status)}
</Badge>
// Extract conditional branches to separate components
// Instead of: {isLoading ? <Spinner /> : <Content />}
// Create: <LoadingState /> and <ContentState /> components with props
AI will confidently write code that:
Trust AI for:
Scrutinize AI for:
When in doubt, ask: "Can I easily test this component's different states?"
If the answer is no, refactor until you can pass props to control each state independently.