Component Architecture
MedComponent architecture defines how you decompose a complex UI into a hierarchy of reusable, maintainable components. In frontend system design, this means deciding component boundaries, defining prop contracts, choosing composition patterns, and structuring the component tree for testability and performance.
Interactive Visualization
Key Points
- Single Responsibility Principle: each component should have one reason to change
- Container/Presentational split separates data-fetching logic from rendering
- Compound components share implicit state via context for flexible composition
- Controlled vs uncontrolled components determine who owns the state
- Component boundaries should align with data boundaries to minimize prop drilling
- Use composition over configuration: children and render props beat mega-prop components
- Design leaf components to be stateless and pure for maximum reusability
- Barrel exports (index.ts) provide clean public APIs for component modules
Code Examples
Container/Presentational Split
// Container: owns data and logic function UserListContainer() { const { data, isLoading, error } = useQuery(['users'], fetchUsers) const [sortBy, setSortBy] = useState<'name' | 'date'>('name') const sorted = useMemo( () => sortUsers(data ?? [], sortBy), [data, sortBy] ) if (error) return <ErrorState error={error} /> if (isLoading) return <UserListSkeleton /> return ( <UserList users={sorted} sortBy={sortBy} onSortChange={setSortBy} /> ) } // Presentational: pure rendering, no data fetching interface UserListProps { users: User[] sortBy: 'name' | 'date' onSortChange: (sort: 'name' | 'date') => void } function UserList({ users, sortBy, onSortChange }: UserListProps) { return ( <div> <SortControls value={sortBy} onChange={onSortChange} /> {users.map(user => ( <UserCard key={user.id} user={user} /> ))} </div> ) }
Separating data concerns from presentation makes components easier to test, reuse, and reason about. The presentational component can be tested with static props without mocking API calls.
Compound Component Pattern
// Compound components share state via context interface TabsContextValue { activeTab: string setActiveTab: (id: string) => void } const TabsContext = createContext<TabsContextValue | null>(null) function Tabs({ defaultTab, children }: TabsProps) { const [activeTab, setActiveTab] = useState(defaultTab) return ( <TabsContext.Provider value={{ activeTab, setActiveTab }}> <div role="tablist">{children}</div> </TabsContext.Provider> ) } function TabTrigger({ id, children }: TabTriggerProps) { const ctx = useContext(TabsContext)! return ( <button role="tab" aria-selected={ctx.activeTab === id} onClick={() => ctx.setActiveTab(id)} > {children} </button> ) } function TabPanel({ id, children }: TabPanelProps) { const ctx = useContext(TabsContext)! if (ctx.activeTab !== id) return null return <div role="tabpanel">{children}</div> } // Usage: flexible, declarative API <Tabs defaultTab="overview"> <TabTrigger id="overview">Overview</TabTrigger> <TabTrigger id="code">Code</TabTrigger> <TabPanel id="overview">Overview content</TabPanel> <TabPanel id="code">Code content</TabPanel> </Tabs>
Compound components provide a flexible, declarative API where consumers control layout and composition while the parent manages shared state. This pattern avoids prop explosion.
Component Module Structure
// Feature module structure // src/features/UserProfile/ // ├── index.ts (public API) // ├── UserProfile.tsx (main component) // ├── UserProfile.test.tsx (tests) // ├── UserAvatar.tsx (sub-component) // ├── useUserProfile.ts (custom hook) // ├── userProfile.types.ts (local types) // └── UserProfile.module.css // index.ts — clean public API export { UserProfile } from './UserProfile' export type { UserProfileProps } from './userProfile.types' // Shared types stay in src/types/ // Local types stay in the feature module interface UserProfileProps { userId: string variant: 'compact' | 'full' onEdit?: () => void } // Internal sub-components are NOT exported // Only the public API is accessible to other modules
Organizing components into feature modules with barrel exports creates clear public APIs. Internal sub-components stay encapsulated, reducing coupling between modules.
Common Mistakes
- Creating god components with 20+ props instead of composing smaller components
- Mixing data-fetching logic with rendering logic in the same component
- Prop drilling through 4+ levels instead of using context or composition
- Making every component controlled when uncontrolled defaults would simplify the API
- Exposing internal sub-components through the public module API
Interview Tips
- Start by sketching the component tree on a whiteboard before writing code
- Name components after what they render, not what data they fetch
- Discuss trade-offs between composition patterns when relevant
- Show awareness of testing strategy: pure presentational components are easier to test
- Mention accessibility concerns at the component API level (ARIA roles, keyboard navigation)