Component Types & Composition
Building UIs Like LEGO, Not Cement
LinkedIn Hook
React gives you two ways to build components: class and functional.
One of them won. And it was not even close.
But here is the part most developers miss: knowing WHICH type to use is only half the interview question. The other half is knowing how to COMPOSE components together — and why React chose composition over inheritance.
Ever built a component that does 15 things? That is not a component. That is a monolith wearing a trench coat.
In this lesson, I break down functional vs class components, the children prop, compound components pattern, and the exact moment you should split a component — with code examples interviewers love to ask about.
Read the full lesson → [link]
#React #JavaScript #WebDevelopment #InterviewPrep #Frontend #ComponentDesign #ReactHooks
What You'll Learn
- Why functional components replaced class components — and the one thing classes still do
- How composition works in React and why React explicitly recommends it over inheritance
- The
childrenprop and how it enables flexible component design - The compound components pattern that libraries like Radix and Headless UI use
- When a component is doing too much and needs to be split
The LEGO Analogy
Think of React components like LEGO bricks — not cement blocks.
With cement, you pour everything into one mold. You get one big, solid shape. Want to change one wall? You demolish the whole thing.
With LEGO, you build small, reusable pieces that snap together. Want to change one section? Pop out that brick and swap it. The rest stays intact.
React chose the LEGO approach. Every UI is a tree of small components composed together. A <Page> contains a <Header>, a <Sidebar>, and a <Content>. The <Header> contains a <Logo> and a <Nav>. Each piece is independent, reusable, and replaceable.
This is composition — and it is the single most important design principle in React.
Functional vs Class Components
Class Components — The Old Way
Before React 16.8 (February 2019), if you needed state or lifecycle methods, you had to use a class component:
import React, { Component } from "react";
class Greeting extends Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
componentDidMount() {
document.title = `Clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `Clicked ${this.state.count} times`;
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>Hello, {this.props.name}</p>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick}>Click me</button>
</div>
);
}
}
// Output: renders a greeting with a click counter
// Document title updates on every click
Problems with this:
thisbinding is confusing (this.state,this.props,this.setState, arrow functions vs.bind())- Logic is split across lifecycle methods (
componentDidMountandcomponentDidUpdateoften duplicate code) - Classes do not minify well
- Hard to share stateful logic between components (led to HOCs and render props — complex patterns)
Functional Components — The Winner
The same component with hooks:
import { useState, useEffect } from "react";
function Greeting({ name }) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Clicked ${count} times`;
}, [count]);
return (
<div>
<p>Hello, {name}</p>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
// Output: identical behavior — greeting with click counter
// But 60% less code and no `this` anywhere
Why Functional Won
| Aspect | Class | Functional + Hooks |
|---|---|---|
| State | this.state + this.setState | useState |
| Side effects | Split across 3+ lifecycle methods | One useEffect |
| Logic reuse | HOC / render props (complex) | Custom hooks (simple) |
this binding | Yes, constant source of bugs | No this needed |
| Bundle size | Larger (classes don't minify well) | Smaller |
| Learning curve | Steeper | Simpler |
| React team focus | No new class features since 2019 | All new features are hook-based |
The one thing classes still do that hooks cannot: Error Boundaries. As of React 19, there is still no hook equivalent for componentDidCatch and getDerivedStateFromError. You need a class component (or the react-error-boundary library which wraps one for you).
Component Composition vs Inheritance
React's official documentation says it plainly: "We recommend using composition instead of inheritance."
In traditional OOP, you might extend a base class:
// DON'T DO THIS IN REACT
class Button extends Component { /* ... */ }
class DangerButton extends Button { /* ... */ }
class IconButton extends Button { /* ... */ }
class DangerIconButton extends DangerButton { /* wait, or IconButton? */ }
// Inheritance tree gets messy fast
In React, you compose instead:
// Composition — flexible and clear
function Button({ variant, icon, children }) {
const className = variant === "danger" ? "btn-danger" : "btn-default";
return (
<button className={className}>
{icon && <span className="btn-icon">{icon}</span>}
{children}
</button>
);
}
// Usage — mix and match freely
<Button>Save</Button>
// Output: <button class="btn-default">Save</button>
<Button variant="danger">Delete</Button>
// Output: <button class="btn-danger">Delete</button>
<Button variant="danger" icon="🗑️">Delete</Button>
// Output: <button class="btn-danger"><span class="btn-icon">🗑️</span>Delete</button>
No inheritance hierarchy. One component, composed with props.
The children Prop
children is a special prop that contains whatever you put between a component's opening and closing tags. It is the backbone of composition in React.
function Card({ title, children }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">
{children}
</div>
</div>
);
}
// Usage
<Card title="User Profile">
<img src="avatar.jpg" alt="avatar" />
<p>Rakibul Hasan</p>
<button>Follow</button>
</Card>
// Output:
// <div class="card">
// <h2>User Profile</h2>
// <div class="card-body">
// <img src="avatar.jpg" alt="avatar" />
// <p>Rakibul Hasan</p>
// <button>Follow</button>
// </div>
// </div>
The Card component does not know or care what its children are. It just renders them. This is specialization through composition — the Card provides the frame, the consumer fills the content.
Multiple Slots (Named Children via Props)
Sometimes you need more than one slot. Use regular props:
function Layout({ sidebar, children }) {
return (
<div className="layout">
<aside className="sidebar">{sidebar}</aside>
<main className="content">{children}</main>
</div>
);
}
// Usage
<Layout sidebar={<Nav links={["Home", "About", "Contact"]} />}>
<h1>Welcome to the site</h1>
<p>Main content goes here</p>
</Layout>
// Output: sidebar on left with Nav, main content on right
This is React's answer to "slots" in Vue or "named yields" in Ember. No special API — just props.
Compound Components Pattern
Compound components are a group of components that work together to form a complete UI, sharing implicit state. Think of <select> and <option> in HTML — they are useless alone but powerful together.
import { createContext, useContext, useState } from "react";
// Shared context for the compound component
const TabsContext = createContext();
function Tabs({ defaultTab, children }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }) {
return <div className="tab-list" role="tablist">{children}</div>;
}
function Tab({ value, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
const isActive = activeTab === value;
return (
<button
role="tab"
className={isActive ? "tab active" : "tab"}
onClick={() => setActiveTab(value)}
>
{children}
</button>
);
}
function TabPanel({ value, children }) {
const { activeTab } = useContext(TabsContext);
if (activeTab !== value) return null;
return <div role="tabpanel" className="tab-panel">{children}</div>;
}
// Attach sub-components to parent
Tabs.TabList = TabList;
Tabs.Tab = Tab;
Tabs.TabPanel = TabPanel;
// Usage — clean, readable, flexible
<Tabs defaultTab="profile">
<Tabs.TabList>
<Tabs.Tab value="profile">Profile</Tabs.Tab>
<Tabs.Tab value="settings">Settings</Tabs.Tab>
<Tabs.Tab value="billing">Billing</Tabs.Tab>
</Tabs.TabList>
<Tabs.TabPanel value="profile">
<p>Your profile info here</p>
</Tabs.TabPanel>
<Tabs.TabPanel value="settings">
<p>App settings here</p>
</Tabs.TabPanel>
<Tabs.TabPanel value="billing">
<p>Billing details here</p>
</Tabs.TabPanel>
</Tabs>
// Output: clicking a tab shows only that tab's panel
Why this pattern matters in interviews:
- Libraries like Radix UI, Headless UI, and Reach UI all use it
- It demonstrates understanding of Context, composition, and API design
- It shows you can build flexible, reusable component APIs
When to Split a Component
A common interview question: "How do you know when a component is too big?"
Here are the signals:
Split When:
- It has multiple responsibilities — A component that fetches data, handles form validation, AND renders a complex UI should be at least 3 components
- You are passing too many props — If a component takes 10+ props, it is probably doing too much
- Part of it re-renders unnecessarily — If only the header changes but the whole page re-renders, extract the header
- You copy-paste JSX — Repeated UI patterns should become a component
- The file is over 200 lines — Not a hard rule, but a strong signal
Do NOT Split When:
- It would create prop drilling — Splitting just to have smaller files but passing 8 props through 3 levels defeats the purpose
- The pieces are never reused — Not every
<div>needs to be its own component - It makes the code harder to follow — If splitting forces readers to jump between 5 files to understand one feature, keep it together
// BEFORE — one component doing everything
function UserDashboard() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [notifications, setNotifications] = useState([]);
useEffect(() => { /* fetch user */ }, []);
useEffect(() => { /* fetch posts */ }, []);
useEffect(() => { /* fetch notifications */ }, []);
return (
<div>
{/* 50 lines of header JSX */}
{/* 80 lines of post list JSX */}
{/* 40 lines of notification panel JSX */}
</div>
);
}
// AFTER — composed from focused components
function UserDashboard() {
return (
<div>
<UserHeader />
<PostList />
<NotificationPanel />
</div>
);
}
// Each child component owns its own data fetching and rendering
// Easier to test, easier to maintain, easier to explain in an interview
Common Mistakes
-
Using inheritance to extend components. React components should never extend other components (except
React.ComponentorReact.PureComponentfor class components). Use composition with props and children instead. If you mention inheritance in a React interview, it is a red flag. -
Ignoring the
childrenprop. Many developers pass content as a regular prop (content={<Something />}) whenchildrenwould make the API much cleaner. If something goes between opening and closing tags, usechildren. -
Over-splitting components too early. Extracting every 5 lines into its own component creates a maze of tiny files with excessive prop passing. Split when you have a reason (reuse, readability, performance), not because "smaller is always better."
Interview Questions
Q: What are the differences between functional and class components?
Q: Can functional components completely replace class components?
Almost. The only feature that still requires a class component is Error Boundaries (
componentDidCatch/getDerivedStateFromError). For everything else, functional components with hooks are the standard.
Q: Why does React recommend composition over inheritance?
Q: What is the compound components pattern and when would you use it?
Compound components are a set of components that share implicit state through Context and work together as a unit (like
<Tabs>+<Tab>+<TabPanel>). Use it when building reusable component libraries where the consumer needs flexible control over structure and layout while the internal state logic is managed automatically.
Q: How do you decide when to split a component into smaller ones?
Quick Reference — Cheat Sheet
COMPONENT TYPES
======================================================
Class Component | Functional Component
-------------------------|----------------------------
class X extends Component| function X(props) { }
this.state / setState | useState()
lifecycle methods | useEffect()
this.props | props (argument)
Still needed for: | Everything else
Error Boundaries |
======================================================
COMPOSITION PATTERNS
======================================================
children prop → Content between <Parent>...</Parent>
Named slots → Pass JSX as regular props
Compound components → Related components + shared Context
======================================================
WHEN TO SPLIT
======================================================
YES: Multiple responsibilities, 10+ props, copy-paste JSX
NO: Creates prop drilling, never reused, harder to read
======================================================
Previous: Lesson 1.3 — JSX — Not HTML → Next: Lesson 2.1 — Props — Passing Data Down →
This is Lesson 1.4 of the React Interview Prep Course — 10 chapters, 42 lessons.