Component Design Principles
Component Design Principles
Effective component architecture follows key design principles that promote reusability, maintainability, and testability.
Single Responsibility
Each component should have one reason to change. A component that handles both display logic and data fetching violates SRP.
// Bad: Component does too much
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <Spinner />;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
// Good: Separated concerns
function UserProfile({ user }) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
function UserProfileContainer({ userId }) {
const { user, loading } = useUser(userId);
if (loading) return <Spinner />;
return <UserProfile user={user} />;
}
Open/Closed Principle
Components should be open for extension but closed for modification. Use composition over configuration props.
// Closed for modification, open for extension
function Card({ children }) {
return <div className="card">{children}</div>;
}
Card.Header = function CardHeader({ children }) {
return <div className="card-header">{children}</div>;
};
Card.Body = function CardBody({ children }) {
return <div className="card-body">{children}</div>;
};
// Usage
<Card>
<Card.Header>Title</Card.Header>
<Card.Body>Content</Card.Body>
</Card>
Liskov Substitution
Components should be replaceable with others that share the same interface. If you have a Button component, any variant should work in the same contexts.
Interface Segregation
Don't force components to depend on props they don't use. Create specific interfaces:
// Instead of one massive component
function MediaCard({ title, description, imageUrl, videoUrl, type }) { ... }
// Create focused components
function ImageCard({ title, description, imageUrl }) { ... }
function VideoCard({ title, description, videoUrl }) { ... }
Dependency Inversion
Depend on abstractions, not concretions. Use dependency injection for services:
// Inject dependencies
function DataList({ fetchFn, renderItem }) {
const [data, setData] = useState([]);
useEffect(() => {
fetchFn().then(setData);
}, [fetchFn]);
return <ul>{data.map(renderItem)}</ul>;
}
// Usage with different data sources
<DataList fetchFn={fetchUsers} renderItem={renderUser} />
<DataList fetchFn={fetchProducts} renderItem={renderProduct} />
Container vs Presentational
Container vs Presentational Components
This pattern separates data logic from presentation, making components more reusable and easier to test.
Presentational Components
These components focus on how things look. They receive data via props and call callbacks passed from parents.
// Pure presentational component
function TodoList({ todos, onToggle, onDelete }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id} className={todo.completed ? 'done' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
);
}
Container Components
These handle data fetching, state management, and business logic:
function TodoListContainer() {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchTodos()
.then(setTodos)
.finally(() => setLoading(false));
}, []);
const handleToggle = async (id) => {
const updated = todos.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
);
setTodos(updated);
await toggleTodo(id);
};
const handleDelete = async (id) => {
setTodos(todos.filter(t => t.id !== id));
await deleteTodo(id);
};
if (loading) return <Spinner />;
return (
<TodoList
todos={todos}
onToggle={handleToggle}
onDelete={handleDelete}
/>
);
}
Benefits
- Testability: Test presentational components with mock data
- Reusability: Presentational components work with any data source
- Separation of Concerns: Clear boundary between logic and UI
When to Use
Use this pattern when components have complex data dependencies or when the same UI needs different data sources. For simple components, mixing concerns is acceptable.
Component Patterns
Component Patterns
Compound Components
Manage shared state implicitly through context:
const TabsContext = createContext();
function Tabs({ children, defaultTab }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
Tabs.Tab = function Tab({ id, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
return (
<button
className={activeTab === id ? 'active' : ''}
onClick={() => setActiveTab(id)}
>
{children}
</button>
);
};
Tabs.Panel = function Panel({ id, children }) {
const { activeTab } = useContext(TabsContext);
if (activeTab !== id) return null;
return <div className="tab-panel">{children}</div>;
};
// Usage
<Tabs defaultTab="tab1">
<Tabs.Tab id="tab1">Tab 1</Tabs.Tab>
<Tabs.Tab id="tab2">Tab 2</Tabs.Tab>
<Tabs.Panel id="tab1">Content 1</Tabs.Panel>
<Tabs.Panel id="tab2">Content 2</Tabs.Panel>
</Tabs>
Render Props
Pass a function as a child that receives data:
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
return (
<div onMouseMove={handleMouseMove} style={{ height: '100vh' }}>
{render(position)}
</div>
);
}
// Usage
<MouseTracker render={({ x, y }) => (
<p>Mouse at: {x}, {y}</p>
)} />
Higher-Order Components (HOC)
Wrap components to add behavior:
function withLoading(WrappedComponent) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) return <Spinner />;
return <WrappedComponent {...props} />;
};
}
const UserListWithLoading = withLoading(UserList);
// Usage
<UserListWithLoading isLoading={loading} users={users} />
Custom Hooks
The modern alternative to HOCs and render props:
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return position;
}
// Usage
function App() {
const { x, y } = useMousePosition();
return <p>Mouse at: {x}, {y}</p>;
}
Practice Problems
Create a reusable React component implementing Component Architecture. Include proper state management and accessibility.
Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where neededWrite unit and integration tests for Component Architecture using React Testing Library.
Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility testsOptimize Component Architecture for performance. Consider memoization, code splitting, and bundle size.
Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysisQuiz
1. Which principle states that a component should have only one reason to change?
2. What is the main difference between container and presentational components?
3. Which pattern manages shared state implicitly through React Context?
4. What is the primary benefit of the container/presentational pattern?
Flashcards
Question
What is the Single Responsibility Principle in component design?
Click to reveal answer
Answer
Each component should have one job or reason to change, keeping concerns separated.
Question
What are Container Components?
Click to reveal answer
Answer
Components that handle data fetching, state management, and business logic, wrapping presentational components.
Question
What is a Compound Component pattern?
Click to reveal answer
Answer
A pattern where related components share implicit state through Context, allowing flexible composition.
Question
What is the Render Props pattern?
Click to reveal answer
Answer
Passing a function as a child (or prop) that receives data and returns JSX, enabling component reuse.
Question
What is Component Architecture?
Click to reveal answer
Answer
Component Architecture is a key concept in frontend development.
Revision Notes
Key Takeaways
- 1.Apply SOLID principles to component design for maintainability
- 2.Separate data logic from presentation using container/presentational pattern
- 3.Use Compound Components for related component groups sharing state
- 4.Custom Hooks are the modern alternative to HOCs and render props
- 5.Composition is preferred over configuration props
Interview Tips
- •Be able to explain when to use container vs presentational components
- •Know the trade-offs between HOCs, render props, and custom hooks
- •Discuss how you would refactor a large component following SOLID principles
Cheat Sheet
Component Architecture Cheat Sheet
Design Principles
- SRP: One component, one job
- OCP: Extend via composition, not modification
- LSP: Subtypes must be substitutable
- ISP: Don't force unused props
- DIP: Depend on abstractions
Patterns
- Container/Presentational: Logic vs UI separation
- Compound Components: Shared context state
- Render Props: Function as child
- HOC: Wrap to add behavior
- Custom Hooks: Modern stateful logic reuse
Code Smells
- God components (too many responsibilities)
- Prop drilling (passing through many levels)
- Mega components (handling UI + data + logic)