Must knowAdvanced React Patterns · 01

Composition and Layout Components

Pass finished elements, not data, through components that only lay things out.

On this page · 7 sections
  1. In one minute
  2. The example: the product page
  3. How it works
  4. Common mistakes
  5. Interview Q&A
  6. Related
  7. Sources

In one minute

Prop drilling is passing a prop through components that don’t use it, so that a component further down can. It often happens with layout components: a Nav, a Main, a Footer whose only job is to decide where things go. The fix is to stop passing them data and pass them finished elements instead. The component that owns the data builds the element (<img src={user.avatar} />), and the layout component just places it. For one region that’s children; for several regions, named props like sidebar and content.

You’ll be able to: spot a layout component that’s forwarding data, and turn it into one that takes elements.

Data drilled
App.tsx
1function App() {2  const [user] = useState(sam)3  const [selected, setSelected] = useState(null)4  return (<>5    <Nav user={user} />6    <Main products={products} selected={selected}7      onSelect={setSelected} />8    <Footer user={user} />9  </>)10}1112function Nav({ user }) {13  return <nav><img src={user.avatar} /></nav>14}
Props received (recorded)
Nav–
Main–
Footer–
  1. render phaseconst [user] = useState(sam)App owns the user, the products and the selected product.
  2. render phase<Nav user={user} />Nav only places an avatar, yet it has to take the whole user to build it.
  3. render phase<Main products={products} selected={selected}Main only puts two areas side by side, yet it receives three props it just passes on.
  4. render phase<Footer user={user} />Same for Footer. Every layout component now depends on data it doesn’t use: change User and their types change too.

0 / 0
A product page: a nav bar with the shopper’s avatar, a product list next to the selected product’s details, and a footer. Which props does each layout component receive? (Both versions recorded.)

The example: the product page

The page has a nav bar with the shopper’s avatar, a main area with a product list and the selected product’s details, and a footer greeting. App owns the user, the products and the selection.

1. Data drilled through the layout

function AppDrilled() {
	const [user] = useState<User>({ name: 'Sam', avatar: '/logo.svg' })
	const [selected, setSelected] = useState<Product | null>(null)
	return (
		<>
			<NavDrilled user={user} />
			<MainDrilled products={products} selected={selected} onSelect={setSelected} />
			<FooterDrilled user={user} />
		</>
	)
}

function NavDrilled(p: { user: User }) {
	props('Nav', p)
	return <nav><img src={p.user.avatar} alt={`${p.user.name}'s profile`} /></nav>
}

function MainDrilled(p: { products: Product[]; selected: Product | null; onSelect: (p: Product) => void }) {
	props('Main', p)
	return (
		<main>
			<ProductList products={p.products} onSelect={p.onSelect} />
			<ProductDetails product={p.selected} />
		</main>
	)
}

function FooterDrilled(p: { user: User }) {
	props('Footer', p)
	return <footer>Happy shopping, {p.user.name}!</footer>
}

What each layout component received:

Nav got: user
Main got: products, selected, onSelect
Footer got: user

Nav and Footer take the whole user to show an image and a name. Main takes three props it never reads, only passes on.

2. Elements built where the data is

function AppComposed() {
	const [user] = useState<User>({ name: 'Sam', avatar: '/logo.svg' })
	const [selected, setSelected] = useState<Product | null>(null)
	return (
		<>
			<Nav avatar={<img src={user.avatar} alt={`${user.name}'s profile`} />} />
			<Main
				sidebar={<ProductList products={products} onSelect={setSelected} />}
				content={<ProductDetails product={selected} />}
			/>
			<Footer>Happy shopping, {user.name}!</Footer>
		</>
	)
}

function Nav(p: { avatar: ReactNode }) {
	props('Nav', p)
	return <nav>{p.avatar}</nav>
}

function Main(p: { sidebar: ReactNode; content: ReactNode }) {
	props('Main', p)
	return (
		<main>
			{p.sidebar}
			{p.content}
		</main>
	)
}

function Footer(p: { children: ReactNode }) {
	props('Footer', p)
	return <footer>{p.children}</footer>
}
Nav got: avatar
Main got: sidebar, content
Footer got: children

None of the layout components sees user, the products or setSelected any more. Both versions behave the same (clicking Desk Lamp showed Desk Lamp: $39.00 in each). What changed is the dependencies: if User gains a field, or the list needs a new prop, only App and the component that actually uses it change.

The components that really use the data, ProductList and ProductDetails, still take normal props. The pattern is for the components in between.

How it works

Common mistakes

Interview Q&A

What is prop drilling, and why is it a problem?

Passing props through components that don’t use them, just to reach a descendant. Every component in the chain then depends on data it doesn’t care about, so changes to that data ripple through all of them. Recorded: Main received products, selected, onSelect and used none of them.

How does composition fix it?

Build the element where the data lives and pass the element down. Layout components take ReactNode props (or children) and only decide where to render them. Recorded: Nav got avatar, Main got sidebar, content, Footer got children: no data at all.

When do you use children vs named element props?

children when there’s one region to fill. Named props like sidebar and content when a layout has several independent regions.

Isn't this just context with extra steps?

No. Context shares a value with many components implicitly. Composition places a specific piece of UI. Reach for composition first: it adds no provider, no hook, and no extra re-renders.

When does it stop paying off?

When a layout component’s list of element props gets long and hard to follow. Then the pieces often belong together as compound components or slots.

Sources

Primary source: kentcdodds.com/blog/prop-drilling