Component Composition

Component Composition

Component composition is a powerful pattern in React that allows you to build complex UIs by combining simpler components. It promotes reusability and maintainability.

What is Composition?

Composition is about building components from other components, using props to pass data and functionality. It's React's recommended approach over inheritance.

// Instead of inheritance
// class SpecialButton extends Button { } ❌

// Use composition
function SpecialButton(props) {
  return (
    <Button color="red" size="large" {...props}>
      <span>✨</span> {props.children}
    </Button>
  );
}

Basic Composition

Composing Simple Components

function Avatar({ src, alt, size = 50 }) {
  return (
    <img 
      src={src} 
      alt={alt}
      style={{ 
        width: size, 
        height: size, 
        borderRadius: '50%' 
      }}
    />
  );
}

function UserInfo({ name, title }) {
  return (
    <div className="user-info">
      <h3>{name}</h3>
      <p>{title}</p>
    </div>
  );
}

function UserCard({ user }) {
  return (
    <div className="user-card">
      <Avatar src={user.avatar} alt={user.name} />
      <UserInfo name={user.name} title={user.title} />
    </div>
  );
}

Children Props

The special children prop allows components to be composed like HTML elements:

function Card({ children, title }) {
  return (
    <div className="card">
      {title && <h2 className="card-title">{title}</h2>}
      <div className="card-content">
        {children}
      </div>
    </div>
  );
}

function App() {
  return (
    <Card title="User Profile">
      <p>Name: John Doe</p>
      <p>Email: [email protected]</p>
      <button>Edit Profile</button>
    </Card>
  );
}

Multiple Composition Slots

Components can have multiple slots for composition:

function Layout({ header, sidebar, content, footer }) {
  return (
    <div className="layout">
      <header className="header">{header}</header>
      <div className="main">
        <aside className="sidebar">{sidebar}</aside>
        <main className="content">{content}</main>
      </div>
      <footer className="footer">{footer}</footer>
    </div>
  );
}

function App() {
  return (
    <Layout
      header={<Header />}
      sidebar={<Navigation />}
      content={<MainContent />}
      footer={<Footer />}
    />
  );
}

Specialized Components

Create specialized versions of generic components:

// Generic button component
function Button({ variant, size, children, ...props }) {
  return (
    <button 
      className={`btn btn-${variant} btn-${size}`}
      {...props}
    >
      {children}
    </button>
  );
}

// Specialized buttons
function PrimaryButton(props) {
  return <Button variant="primary" {...props} />;
}

function DangerButton(props) {
  return <Button variant="danger" {...props} />;
}

function IconButton({ icon, children, ...props }) {
  return (
    <Button {...props}>
      <span className="icon">{icon}</span>
      {children}
    </Button>
  );
}

Compound Components

Components that work together to form a cohesive unit:

// Tab component system
function Tabs({ children, defaultTab = 0 }) {
  const [activeTab, setActiveTab] = useState(defaultTab);
  
  return (
    <div className="tabs">
      {React.Children.map(children, (child) =>
        React.cloneElement(child, { activeTab, setActiveTab })
      )}
    </div>
  );
}

function TabList({ children, activeTab, setActiveTab }) {
  return (
    <div className="tab-list">
      {React.Children.map(children, (child, index) =>
        React.cloneElement(child, {
          isActive: activeTab === index,
          onClick: () => setActiveTab(index)
        })
      )}
    </div>
  );
}

function Tab({ children, isActive, onClick }) {
  return (
    <button 
      className={`tab ${isActive ? 'active' : ''}`}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

function TabPanels({ children, activeTab }) {
  return (
    <div className="tab-panels">
      {React.Children.toArray(children)[activeTab]}
    </div>
  );
}

function TabPanel({ children }) {
  return <div className="tab-panel">{children}</div>;
}

// Usage
function App() {
  return (
    <Tabs defaultTab={0}>
      <TabList>
        <Tab>Profile</Tab>
        <Tab>Settings</Tab>
        <Tab>Notifications</Tab>
      </TabList>
      <TabPanels>
        <TabPanel>Profile content...</TabPanel>
        <TabPanel>Settings content...</TabPanel>
        <TabPanel>Notifications content...</TabPanel>
      </TabPanels>
    </Tabs>
  );
}

Render Props Pattern

Pass a function as a prop to share code between components:

function MouseTracker({ render }) {
  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 render(position);
}

// Usage
function App() {
  return (
    <div>
      <MouseTracker 
        render={({ x, y }) => (
          <p>Mouse position: {x}, {y}</p>
        )}
      />
      
      <MouseTracker 
        render={({ x, y }) => (
          <div 
            style={{
              position: 'absolute',
              left: x - 10,
              top: y - 10,
              width: 20,
              height: 20,
              backgroundColor: 'red',
              borderRadius: '50%'
            }}
          />
        )}
      />
    </div>
  );
}

Props Forwarding

Forward props to child components while adding your own logic:

function FancyInput({ label, error, ...inputProps }) {
  return (
    <div className="form-field">
      {label && <label>{label}</label>}
      <input 
        className={error ? 'error' : ''}
        {...inputProps}
      />
      {error && <span className="error-message">{error}</span>}
    </div>
  );
}

// All standard input props are forwarded
<FancyInput 
  label="Email"
  type="email"
  placeholder="Enter email"
  value={email}
  onChange={handleChange}
  error={emailError}
/>

Component as Props

Pass components as props for maximum flexibility:

function List({ items, ItemComponent, EmptyComponent }) {
  if (items.length === 0) {
    return EmptyComponent ? <EmptyComponent /> : <p>No items</p>;
  }
  
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          <ItemComponent item={item} />
        </li>
      ))}
    </ul>
  );
}

// Different item components
function UserItem({ item }) {
  return <span>{item.name} - {item.email}</span>;
}

function ProductItem({ item }) {
  return <span>{item.name} - ${item.price}</span>;
}

// Usage
<List 
  items={users}
  ItemComponent={UserItem}
  EmptyComponent={() => <p>No users found</p>}
/>

HOC vs Composition

Instead of Higher-Order Components, often composition is clearer:

// Instead of HOC
// const EnhancedComponent = withAuth(MyComponent); ❌

// Use composition
function ProtectedRoute({ component: Component, ...rest }) {
  const isAuthenticated = useAuth();
  
  if (!isAuthenticated) {
    return <Navigate to="/login" />;
  }
  
  return <Component {...rest} />;
}

// Usage
<ProtectedRoute component={Dashboard} />

Real-World Example: Form Builder

function Form({ children, onSubmit }) {
  const [values, setValues] = useState({});
  const [errors, setErrors] = useState({});
  
  const setValue = (name, value) => {
    setValues(prev => ({ ...prev, [name]: value }));
  };
  
  const setError = (name, error) => {
    setErrors(prev => ({ ...prev, [name]: error }));
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    onSubmit(values);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      {React.Children.map(children, child =>
        React.cloneElement(child, {
          setValue,
          setError,
          values,
          errors
        })
      )}
    </form>
  );
}

function Field({ name, label, type = 'text', setValue, values, errors }) {
  return (
    <div className="field">
      <label>{label}</label>
      <input
        type={type}
        value={values[name] || ''}
        onChange={(e) => setValue(name, e.target.value)}
      />
      {errors[name] && <span className="error">{errors[name]}</span>}
    </div>
  );
}

function SubmitButton({ children }) {
  return <button type="submit">{children}</button>;
}

// Usage
function ContactForm() {
  const handleSubmit = (values) => {
    console.log('Form values:', values);
  };
  
  return (
    <Form onSubmit={handleSubmit}>
      <Field name="name" label="Name" />
      <Field name="email" label="Email" type="email" />
      <Field name="message" label="Message" type="textarea" />
      <SubmitButton>Send Message</SubmitButton>
    </Form>
  );
}

Best Practices

1. Keep Components Focused

// Good - Single responsibility
function UserAvatar({ user }) {
  return <img src={user.avatar} alt={user.name} />;
}

function UserName({ user }) {
  return <h3>{user.name}</h3>;
}

// Compose them
function UserHeader({ user }) {
  return (
    <div>
      <UserAvatar user={user} />
      <UserName user={user} />
    </div>
  );
}

2. Use Composition for Variations

// Instead of props for every variation
// <Button primary large icon="star" /> ❌

// Compose variations
<PrimaryButton size="large">
  <Icon name="star" /> Click me
</PrimaryButton>

3. Extract Reusable Patterns

function WithLoading({ loading, children }) {
  if (loading) {
    return <Spinner />;
  }
  return children;
}

function WithError({ error, children }) {
  if (error) {
    return <ErrorMessage error={error} />;
  }
  return children;
}

// Usage
<WithError error={error}>
  <WithLoading loading={loading}>
    <DataDisplay data={data} />
  </WithLoading>
</WithError>

Common Patterns

Container and Presentational

// Container - handles logic
function UserListContainer() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetchUsers().then(data => {
      setUsers(data);
      setLoading(false);
    });
  }, []);
  
  return <UserList users={users} loading={loading} />;
}

// Presentational - handles display
function UserList({ users, loading }) {
  if (loading) return <Loading />;
  
  return (
    <ul>
      {users.map(user => (
        <UserItem key={user.id} user={user} />
      ))}
    </ul>
  );
}

Layout Components

function PageLayout({ children }) {
  return (
    <div className="page">
      <Header />
      <main className="content">
        {children}
      </main>
      <Footer />
    </div>
  );
}

function DashboardLayout({ children }) {
  return (
    <PageLayout>
      <div className="dashboard">
        <Sidebar />
        <div className="dashboard-content">
          {children}
        </div>
      </div>
    </PageLayout>
  );
}

Summary

Component composition is fundamental to React development. It enables:

  • Code reusability
  • Flexible component design
  • Clear component relationships
  • Maintainable code structure
  • Separation of concerns

Master composition patterns and you'll write more maintainable and flexible React applications!