Best Practices

AI Rules for Component Architecture

AI creates 500-line god components that mix data fetching, business logic, and UI rendering. Rules for single responsibility, composition patterns, prop drilling solutions, and component file structure.

8 min read·February 13, 2025

500-line god component mixing fetch, state, logic, and rendering — untestable, unreusable, unreadable

Single responsibility, composition patterns, custom hooks, context API, colocated file structure

AI Builds God Components

AI generates components with: 500+ lines (data fetching, state management, business logic, and rendering in one file), deeply nested JSX (8+ levels of nesting, impossible to follow), prop drilling through 5 layers (passing user from page to deeply nested button), mixed concerns (fetch call next to CSS class next to click handler), and no reusability (component so specific it can only be used in one place). The component works but cannot be tested, reused, or understood by another developer.

Modern component architecture is: single-responsibility (each component does one thing), composed (small components assembled into larger features), logic-extracted (custom hooks separate data/logic from rendering), prop-drill-free (context or composition for deep data passing), and colocated (component, styles, tests, and types in one directory). AI generates none of these.

These rules cover: single responsibility principle for components, container/presentational split, composition via children and render props, context for shared state, custom hooks for logic extraction, and colocated file structure.

Rule 1: Single Responsibility Components

The rule: 'Each component has one job. If you cannot describe the component purpose in one sentence without using the word "and," split it. UserProfile: displays user information. UserProfileEditor: handles profile editing. UserAvatar: renders the user image with fallback. Three focused components instead of one UserSection that displays, edits, and renders the avatar.'

For the size heuristic: 'Components over 150 lines almost always violate single responsibility. Not a hard rule — a 200-line form component with 15 fields may be fine. But a 200-line component with data fetching + transformation + rendering + error handling is three components pretending to be one. The test: can you unit test this component without mocking half the application? If not, it is doing too much.'

AI generates: function Dashboard() { const [users, setUsers] = useState([]); const [posts, setPosts] = useState([]); useEffect(() => { fetch('/api/users')... }, []); useEffect(() => { fetch('/api/posts')... }, []); return (<div>{/* 300 lines of JSX mixing users and posts */}</div>); } — one component fetching two resources and rendering both. Split: DashboardPage (layout), UserList (fetches and renders users), PostFeed (fetches and renders posts). Each testable independently.

  • One sentence, no 'and' — if the description needs 'and,' split the component
  • 150-line heuristic: components over 150 lines likely need splitting
  • Can you unit test without mocking half the app? If not, it does too much
  • Split by concern: data fetching, business logic, rendering
  • Three 50-line components > one 150-line god component
💡 One Sentence, No 'And'

If you cannot describe a component in one sentence without using 'and,' it violates single responsibility. UserProfile displays user info. UserProfileEditor handles editing. UserAvatar renders the image. Three focused components, each independently testable.

Rule 2: Container and Presentational Split

The rule: 'Separate data-aware containers from pure UI presentational components. Container: fetches data, manages state, handles side effects, passes data down. Presentational: receives props, renders UI, emits events up. In modern React with hooks, this often means: a custom hook (logic) + a component (rendering). The hook is the container; the component is the presentation.'

For the hooks-era pattern: 'function useUserProfile(userId: string) { const { data, isLoading, error } = useQuery({ queryKey: ["user", userId], queryFn: () => fetchUser(userId) }); return { user: data, isLoading, error }; }. function UserProfile({ userId }: { userId: string }) { const { user, isLoading, error } = useUserProfile(userId); if (isLoading) return <Skeleton />; if (error) return <ErrorState />; return <ProfileCard user={user} />; }. The hook is testable without rendering. The component is testable with mock data.'

AI generates: data fetching inside the JSX return statement, business logic interleaved with rendering, and no separation between what data the component needs and how it displays that data. The hooks-era split: useUserProfile (logic, testable without React), UserProfile (orchestration), ProfileCard (pure UI, testable with storybook). Three layers, each independently testable.

Rule 3: Composition Over Configuration

The rule: 'Prefer composition (children, slots) over configuration (prop objects, boolean flags). Configuration: <Card title="Users" subtitle="All users" icon="users" showBorder hasShadow isCompact />. Composition: <Card><CardHeader><Icon name="users" /><h2>Users</h2></CardHeader><CardBody>All users</CardBody></Card>. Composition is: more flexible (any content in any slot), more readable (JSX shows the structure), and more extensible (add new content types without new props).'

For the compound component pattern: 'Card, Card.Header, Card.Body, Card.Footer — components that share implicit state but compose explicitly. Implementation: Card provides context, sub-components consume it. The API is: <Card variant="elevated"><Card.Header>Title</Card.Header><Card.Body>Content</Card.Body></Card>. Card.Header knows it is inside an elevated card without the consumer passing variant to every sub-component.'

AI generates: <Modal title="Confirm" description="Are you sure?" confirmText="Yes" cancelText="No" onConfirm={...} onCancel={...} showCloseButton icon="warning" size="md" /> — 10 props for a simple modal. Composition: <Modal><Modal.Header>Confirm</Modal.Header><Modal.Body>Are you sure?</Modal.Body><Modal.Footer><Button>No</Button><Button variant="danger">Yes</Button></Modal.Footer></Modal>. More readable, more flexible, fewer props to document.

⚠️ 10 Props vs Readable JSX

<Modal title='Confirm' description='Are you sure?' confirmText='Yes' cancelText='No' showCloseButton icon='warning' size='md' /> — 10 props. Composition: <Modal><Modal.Header>Confirm</Modal.Header><Modal.Body>Are you sure?</Modal.Body></Modal>. More readable, more flexible, self-documenting.

Rule 4: Context and Composition for Prop Drilling

The rule: 'When data must pass through more than 2 intermediate components that do not use it, you have prop drilling. Solutions in order of preference: (1) Composition — restructure so the data-consuming component is a direct child: <Layout sidebar={<Sidebar user={user} />} />. (2) Context — for truly global data (theme, auth, locale): const UserContext = createContext(). (3) State management (Zustand, Jotai) — for complex shared state with multiple consumers.'

For when to use context: 'Context is ideal for: authentication state (user, permissions), theme (dark/light, color tokens), locale (language, formatting), and feature flags. Context is not ideal for: frequently updating values (context re-renders all consumers), large objects (split into multiple focused contexts), and server state (use TanStack Query instead). One context per concern — ThemeContext, AuthContext, LocaleContext — not one giant AppContext.'

AI generates: <Page user={user}><Layout user={user}><Sidebar user={user}><UserMenu user={user}><Avatar user={user} /></UserMenu></Sidebar></Layout></Page> — user passed through 5 layers. Layout and Sidebar do not use user — they just forward it. With composition: <Page><Layout sidebar={<Sidebar><UserMenu user={user} /></Sidebar>} /></Page>. Or with context: Avatar reads from UserContext directly, no drilling.

  • Composition first: restructure to eliminate intermediate passing
  • Context for global data: auth, theme, locale, feature flags
  • One context per concern — not one giant AppContext
  • State management (Zustand) for complex multi-consumer shared state
  • Never pass data through 3+ components that do not use it

Rule 5: Custom Hooks for Logic Extraction

The rule: 'Extract reusable logic into custom hooks. A custom hook: encapsulates state + effects + derived values, is testable without rendering a component, is reusable across multiple components, and has a clear name starting with use. Examples: useDebounce(value, delay), useLocalStorage(key, initial), useMediaQuery(query), useIntersectionObserver(ref, options). If two components share the same useState + useEffect pattern, extract a hook.'

For hook composition: 'Hooks compose naturally: useAuthenticatedUser() calls useContext(AuthContext) + checks permissions. useSearch(query) calls useDebounce(query) + useQuery(debouncedQuery). usePagination() manages page state + provides navigation functions. Complex behavior emerges from simple, tested hooks composed together. Each hook is 10-30 lines, tested in isolation, reused everywhere.'

AI generates: the same useEffect pattern copy-pasted across 5 components with slight variations. Each copy has slightly different bug patterns. A custom hook: written once, tested once, used 5 times. Bug fixes apply everywhere. New features add to the hook. The hook is the reusability unit in modern React — not the component.

ℹ️ The Hook Is the Reuse Unit

The same useState + useEffect pattern copy-pasted across 5 components. A custom hook: written once, tested once, used 5 times. Bug fixes apply everywhere. In modern React, the hook — not the component — is the primary unit of reusability.

Complete Component Architecture Rules Template

Consolidated rules for component architecture.

  • Single responsibility: one sentence, no 'and' — split if description needs 'and'
  • 150-line heuristic: components over 150 lines likely need splitting
  • Custom hooks for logic: useHook() = state + effects + derived values, testable alone
  • Composition over configuration: children and slots, not boolean prop flags
  • Compound components: Card.Header, Card.Body — shared context, explicit composition
  • Prop drilling solutions: composition first, context for global, Zustand for complex
  • One context per concern: ThemeContext, AuthContext — not AppContext
  • Colocated files: component, hook, test, types in one directory