Component Composition
Component composition is a way to share state between components without adding intermediate props. Instead of passing data through every layer of the tree, you structure the components so the data consumer gets rendered directly by the data owner.
The children prop
Section titled “The children prop”React’s children prop lets a component inject arbitrary JSX into its own render output. This is the simplest form of composition.
function Layout({ children }: { children: React.ReactNode }) { return ( <div className="layout"> <header>My App</header> <main>{children}</main> <footer>Footer</footer> </div> )}
// Usagefunction Page() { return ( <Layout> <Dashboard /> </Layout> )}Layout doesn’t know about Dashboard. It doesn’t need to. The state and props that Dashboard requires are handled by Page, the component that builds the composition. This avoids drilling props like dashboardData through Layout just so Layout can pass them down.
Render props
Section titled “Render props”A render prop is a function prop that a component calls to decide what to render. The parent controls the rendering while the child controls the environment.
function MouseTracker({ render }: { render: (pos: { x: number; y: number }) => React.ReactNode }) { const [position, setPosition] = useState({ x: 0, y: 0 })
useEffect(() => { const handler = (e: MouseEvent) => setPosition({ x: e.clientX, y: e.clientY }) window.addEventListener('mousemove', handler) return () => window.removeEventListener('mousemove', handler) }, [])
return <div>{render(position)}</div>}
// Usagefunction App() { return ( <MouseTracker render={({ x, y }) => ( <p>Mouse is at {x}, {y}</p> )} /> )}The caller gets the mouse position without MouseTracker knowing anything about how it’s displayed. Render props invert the ownership of the UI — the data source stays generic, and the consumer decides the rendering.
This pattern was more common before hooks. useMousePosition() is now a simpler abstraction. But render props still appear in existing codebases and library APIs (React Router’s <Route render={...}> variant, for example).
Slots are named children. Instead of a single children prop, a component accepts multiple props, each containing JSX for a specific region.
function Card({ title, body, footer }: { title: React.ReactNode body: React.ReactNode footer?: React.ReactNode}) { return ( <div className="card"> <div className="card-title">{title}</div> <div className="card-body">{body}</div> {footer && <div className="card-footer">{footer}</div>} </div> )}
function ProfileCard({ user }: { user: User }) { return ( <Card title={<Avatar user={user} />} body={<UserDetails user={user} />} footer={<UserActions user={user} />} /> )}Each slot gets its own props directly from the composition caller. No intermediate component passes through props it doesn’t need. The Card component is a pure layout shell.
When composition beats lifting
Section titled “When composition beats lifting”Composition and lifting state up both solve the sibling-communication problem, but they do it differently. Lifting moves state to a parent and drills it down. Composition moves the consumer up — the data owner renders the data consumer directly, bypassing intermediates.
The decision rule: if an intermediate component exists only to forward props, replace it with composition. Instead of <Layout><Sidebar><NavItem /></Sidebar></Layout> with props going through Layout and Sidebar, have the parent that owns the state render the NavItem directly and pass it as a child prop.
// Before: drilling theme through Layout and Sidebarfunction App() { const [theme] = useState('dark') return <Layout theme={theme} />}
// After: compositionfunction App() { const [theme] = useState('dark') return ( <Layout> <NavItem theme={theme} /> </Layout> )}