[Advanced React] React Component Composition: Effective Advanced Techniques

If building a React app is like assembling LEGO blocks, then Component Composition is the art that lets you create masterpieces from the smallest bricks. Anyone can pass props, but to build flexible, reusable, and highly extensible components, you need to master advanced composition patterns.

React Component Composition: Effective Advanced Techniques

This article will help you go from a "builder" to a true "architect" in React. Let's explore how to turn complex, messy components into elegant and powerful structures.

Why "Composition" Over "Inheritance"?

Before diving deeper, remember React's golden philosophy: Composition over Inheritance.

Instead of creating complex inheritance chains (Component B extends A, C extends B), React encourages us to build specialized, small components and "plug" them together like puzzle pieces. This approach helps avoid issues like:

  • Prop Drilling: Passing props through many layers of components that don't need them.
  • Poor reusability: Logic and UI are tightly coupled.
  • Rigid structure: Hard to change or extend later.

Now, buckle up and let's explore composition patterns that will change the way you write React forever!

1. Containment & Specialization

This is the most basic but extremely powerful composition pattern, often overlooked. It uses props.children to create flexible "containers".

🤔 The Problem:

You have a Card component, but its content could be anything: an article, a product, a user profile... How can Card avoid caring about the details inside?

✨ Solution: Use props.children

The Card component just defines the frame (border, shadow, etc.) and leaves a "hole" for content to be passed in via props.children.

// Card.js
function Card(props) {
  return <div className="card">{props.children}</div>
}

// App.js
function App() {
  return (
    <Card>
      <h1>Welcome to the Blog!</h1>
      <p>This is an example of the Containment Pattern.</p>
    </Card>
  )
}

🧠 Going Further with "Slots" (Specialization)

What if you need more than one "hole"? For example, a Dialog needs a title, message, and actions (buttons).

// Dialog.js
function Dialog(props) {
  return (
    <div className="dialog-container">
      <header className="dialog-header">{props.title}</header>
      <main className="dialog-body">{props.message}</main>
      <footer className="dialog-footer">{props.actions}</footer>
    </div>
  )
}

// App.js
function App() {
  return (
    <Dialog
      title={<h1>Warning!</h1>}
      message={<p>Are you sure you want to delete?</p>}
      actions={
        <>
          <button>Cancel</button>
          <button>Delete</button>
        </>
      }
    />
  )
}

When to use: When you want to create generic layout components (Modal, Card, Sidebar, PageLayout) without caring about their specific content.

2. Higher-Order Components (HOCs)

HOC is a classic React concept. It's a function that takes a component and returns a new component with added logic or props.

🤔 The Problem:

Many components (e.g., UserProfile, CommentList, Article) need to fetch data from an API. Should we repeat fetch logic everywhere?

✨ Solution: Create a withData HOC

This HOC wraps the original component, handles data fetching, and passes the result as props.

// withData.js
import React, { useState, useEffect } from 'react'

function withData(WrappedComponent, dataSource) {
  return function (props) {
    const [data, setData] = useState(null)

    useEffect(() => {
      fetch(dataSource)
        .then((res) => res.json())
        .then((data) => setData(data))
    }, [])

    if (!data) {
      return <div>Loading...</div>
    }

    return <WrappedComponent data={data} {...props} />
  }
}

// CommentList.js
function CommentList({ data, ...props }) {
  return (
    <ul>
      {data.map((comment) => (
        <li key={comment.id}>{comment.body}</li>
      ))}
    </ul>
  )
}

export const CommentListWithData = withData(
  CommentList,
  'https://api.example.com/comments',
)

When to use:

  • Sharing logic across multiple components (cross-cutting concerns) like authentication, logging, data fetching.
  • Manipulating props before passing them to the wrapped component.

✅ Pros: Logic is highly reusable.

❌ Cons: Can cause "wrapper hell" (too many nested components in React DevTools), making it hard to track props.

3. Render Props

Render Props is a powerful technique for sharing logic by passing a function as a prop. The component calls this function and provides it with the data needed to render the UI.

🤔 The Problem:

You want a MouseTracker component to track mouse position, but different components may want to display that position differently (show coordinates, move an image, etc.).

✨ Solution: Use a render prop

The MouseTracker manages the mouse logic and calls props.render with the (x, y) coordinates. The parent decides what to render.

// MouseTracker.js
import React, { useState } from 'react'

function MouseTracker(props) {
  const [position, setPosition] = useState({ x: 0, y: 0 })

  const handleMouseMove = (event) => {
    setPosition({
      x: event.clientX,
      y: event.clientY,
    })
  }

  return (
    <div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
      {props.render(position)}
    </div>
  )
}

// App.js
function App() {
  return (
    <div>
      <h1>Move your mouse around!</h1>
      <MouseTracker
        render={({ x, y }) => (
          <p>
            Mouse position: ({x}, {y})
          </p>
        )}
      />
    </div>
  )
}

Note: The prop doesn't have to be called render. Often, people use children as a function (<MouseTracker>{mouse => ...}</MouseTracker>), known as the "Function as Children Pattern".

When to use:

  • When you want to share state or dynamic logic with other components.
  • As a flexible alternative to HOCs, avoiding "wrapper hell".

✅ Pros: Very clear prop origins, highly flexible.

❌ Cons: Can create nested JSX structures.

4. Custom Hooks (the modern king)

With the advent of React Hooks, Custom Hooks have become the most modern and preferred way to share stateful logic between components. They elegantly solve the problems of HOCs and Render Props.

🤔 The Problem:

You want to share logic (fetching data, tracking mouse, accessing local storage...), but want a simple way—no wrappers, no render props.

✨ Solution: Create a Custom Hook

A Custom Hook is just a JavaScript function starting with use that can call other Hooks inside (useState, useEffect, etc.).

// useMousePosition.js
import { useState, useEffect } from 'react'

export 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
}

// App.js
import { useMousePosition } from './useMousePosition'

function App() {
  const { x, y } = useMousePosition()

  return (
    <div>
      <h1>
        Current mouse position: ({x}, {y})
      </h1>
    </div>
  )
}

When to use: Almost always when you want to share stateful logic. This is usually the top choice in modern React.

✅ Pros: Super simple, no extra wrappers, easy to read and test.

❌ Cons: Only works in Function Components.

5. Compound Components

This pattern lets you create components that are implicitly related and work as a unified whole, providing a very expressive and flexible API for users. Think of the <select> and <option> pair in HTML.

🤔 The Problem:

You want to build a complex Tabs component. Users need full control over the structure and style of each Tab and TabPanel, but want the tab switching logic managed automatically.

✨ Solution: Use React Context

Create a parent component (Tabs) that manages all state (which tab is active) via Context. Child components (Tab, TabPanel) read from that Context to know how to render themselves.

// Tabs.js
import React, { useState, useContext, createContext } from 'react'

const TabContext = createContext()

export function Tabs({ children }) {
  const [activeTab, setActiveTab] = useState('tab1')
  const value = { activeTab, setActiveTab }

  return <TabContext.Provider value={value}>{children}</TabContext.Provider>
}

export function TabList({ children }) {
  return <div className="tab-list">{children}</div>
}

export function Tab({ id, children }) {
  const { activeTab, setActiveTab } = useContext(TabContext)
  const isActive = activeTab === id
  return (
    <button
      onClick={() => setActiveTab(id)}
      className={isActive ? 'active' : ''}
    >
      {children}
    </button>
  )
}

export function TabPanel({ id, children }) {
  const { activeTab } = useContext(TabContext)
  return activeTab === id ? <div>{children}</div> : null
}

// App.js
import { Tabs, TabList, Tab, TabPanel } from './Tabs'

function App() {
  return (
    <Tabs>
      <TabList>
        <Tab id="tab1">Tab 1</Tab>
        <Tab id="tab2">Tab 2</Tab>
      </TabList>
      <TabPanel id="tab1">
        <p>This is the content of Tab 1.</p>
      </TabPanel>
      <TabPanel id="tab2">
        <p>And this is the content of Tab 2.</p>
      </TabPanel>
    </Tabs>
  )
}

When to use: When building complex UI component libraries that need a declarative API, like Tabs, Accordion, Dropdown Menu, etc.

Summary: When to Use What?

PatternBest Use CasePros
Containment/SlotsBuilding layout components (Card, Modal, Dialog).Simple, intuitive.
Higher-Order Comp.Add shared logic to Class Components or legacy codebases.Great for logic reuse.
Render PropsShare dynamic state when you need maximum render control.Clear, powerful.
Custom HooksDefault choice for sharing stateful logic in modern React.Clean, simple, no wrappers.
Compound Comp.Building complex UI systems with expressive APIs.Declarative, flexible for users.

Mastering these component composition patterns not only makes your code cleaner and more maintainable, but also opens up a new way of thinking about app structure. Start applying them in your next project and you'll see the magic.

Happy coding with React!

Related Posts

[Advanced React] React Concurrent Mode: Taking App Performance to the Next Level

React Concurrent Mode is a crucial feature that enables React apps to handle multiple tasks at once. Learn how it works and how to implement it to optimize your app’s performance.

[React Basics] React Context: Concept & Most Effective Usage

What is React Context? Learn how to use the React Context API to manage global state easily and efficiently, eliminate Prop Drilling, and optimize your React apps.

[Advanced React] Virtual DOM in React: The Foundation of Performance and How It Works

A deep dive into the Virtual DOM in React—one of the most important concepts for fast rendering. Understand how it works, its key advantages, and why it’s so popular.

[Advanced React] What is React Fiber? Understanding Its Architecture and How It Works

Learn about React Fiber, the core architecture that makes React faster and smoother. Discover how it works, its benefits, and its importance in optimizing React app performance.