Skip to content
intermediatePhase 37 · Frontend Architecture

Component Architecture

Design scalable component hierarchies with clear responsibilities.

45m
0 problems
Topic Progress0%

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

0/3solved
Build Component Architecture Component

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 needed
Component Architecture Testing

Write 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 tests
Component Architecture Performance

Optimize 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 analysis

Quiz

1. Which principle states that a component should have only one reason to change?

Question 1 options

2. What is the main difference between container and presentational components?

Question 2 options

3. Which pattern manages shared state implicitly through React Context?

Question 3 options

4. What is the primary benefit of the container/presentational pattern?

Question 4 options

Flashcards

Question

What is the Single Responsibility Principle in component design?

Answer

Each component should have one job or reason to change, keeping concerns separated.

Question

What are Container Components?

Answer

Components that handle data fetching, state management, and business logic, wrapping presentational components.

Question

What is a Compound Component pattern?

Answer

A pattern where related components share implicit state through Context, allowing flexible composition.

Question

What is the Render Props pattern?

Answer

Passing a function as a child (or prop) that receives data and returns JSX, enabling component reuse.

Question

What is Component Architecture?

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)