Skip to main content

Command Palette

Search for a command to run...

1.12 Composable Components in React

Updated
5 min readView as Markdown

When building large-scale frontend applications, putting entire sections of UI into a single, massive component makes your code rigid and incredibly hard to maintain. To build scalable architectures, we use Composable Components.

This architectural design pattern focuses on breaking complex user interfaces down into small, highly specialized, and deeply reusable building blocks.


1. What are Composable Components?

Component Composition is the practice of combining smaller, independent components to form a larger, more complex piece of UI. Instead of writing all your markup, styling, and event handlers inside one giant file, you delegate specific responsibilities to micro-components.

A Real-World Example: User Profiles

Imagine you need to display a User Profile card that includes an avatar image, a user's name, their job title, and a "Follow" button.

  • The Anti-Pattern (Monolithic Approach): Building one single UserProfile component that handles image rendering, name text manipulation, layout styling, and follow-click API logic.

  • The Scalable Pattern (Composable Approach): Designing a master UserProfile container that coordinates separate, independent building blocks like ProfileAvatar, ProfileName, and FollowButton.

Why Break Down Something Simple Like a Name?

You might think creating a standalone component just to display a string of text like a user's name is over-engineering. However, a dedicated ProfileName component allows you to centralized features like:

  • Handling text overflow if a name is exceptionally long (e.g., truncating it with ellipsis).

  • Formatting capitalization automatically (e.g., ensuring the first letters are always uppercase).

  • Applying strict typography and color themes consistently across the entire platform.

Think of platforms like LinkedIn. A user's profile image appears in the sidebar, directly on the feed posts, inside search lists, and inside chat boxes. Instead of rewriting image tags and CSS borders repeatedly, you write one core component and pass varying size dimensions down via props.


2. Setting Up a Shared Component Architecture

To make components truly composable across an entire system, they should not be hidden inside a specific feature directory. Instead, move them into a global Shared (or Common) Layer so any view can access them.

src/
├── shared/
│   └── components/
│       ├── ProfileName.jsx   <-- Global Reusable Piece
│       └── FollowButton.jsx  <-- Global Reusable Piece
├── features/
│   └── profile/
│       └── UserProfile.jsx   <-- Feature Orchestrator

3. Implementing Component Composition

Step 1: Build the Micro-Components (Shared Blocks)

First, create the small text handler component that safely formats incoming names:

// src/shared/components/ProfileName.jsx
export default function ProfileName({ name }) {
  // Centralized logic: format text or handle edge-case styling safely
  const formattedName = name ? name.trim() : "Guest User";
  
  return <h2 className="profile-name-text">{formattedName}</h2>;
}

Next, build the button block that strictly executes whatever abstract function is passed to it:

// src/shared/components/FollowButton.jsx
export default function FollowButton({ onFollowClick }) {
  return (
    <button className="follow-btn" onClick={onFollowClick}>
      Follow
    </button>
  );
}

Step 2: Assemble the Master Component (The Wrapper)

Now, import these micro-components into your main feature file. The parent component manages the raw data and state configurations, then passes them downward.

// src/features/profile/UserProfile.jsx
import ProfileName from '../../shared/components/ProfileName';
import FollowButton from '../../shared/components/FollowButton';

export default function UserProfile() {
  const userData = {
    username: "john_doe",
    role: "Frontend Engineer",
    avatarUrl: "https://via.placeholder.com/150"
  };

  const handleFollowAction = () => {
    alert(`Now following ${userData.username}`);
  };

  return (
    <div className="profile-card-border">
      <img src={userData.avatarUrl} alt="User Avatar" className="avatar-img" />
      
      {/* Composing our UI using modular building blocks */}
      <ProfileName name={userData.username} />
      <p className="user-role-title">{userData.role}</p>
      <FollowButton onFollowClick={handleFollowAction} />
    </div>
  );
}

Interview Q&A

Q1: What does "Component Composition" mean in React system design?

Answer: Component composition means building complex user interfaces by combining small, focused, independent components instead of writing everything inside one huge file. It is like building with Lego blocks—you create small pieces that do one specific job perfectly and snap them together to create larger features.

Q2: Why should a simple text name or button be turned into an independent component?

Answer: It makes your application easily maintainable. If you need to change how user names are truncated or update the brand color of your primary button, you only have to modify that code in one file instead of hunting down hundreds of scattered instances across your codebase.

Q3: Where should shared composable components be stored in a project directory?

Answer: They should be placed in a globally accessible folder, typically called shared/components/ or common/. If you hide a button or a text formatter inside a specific feature folder like features/profile/, other parts of the app (like the Header or Dashboard) won't be able to easily reuse them.

Q4: How do composable components handle actions like button clicks?

Answer: Composable components remain flexible by accepting functions as props. The child component doesn't know what happens when it is clicked; it just runs a generic trigger like onClick={props.onFollowClick}. The parent component is the one that contains and controls the actual action logic.