Components and Props
Components are the unit of reuse and props are their read-only inputs. See prop drilling pass data through components that don't need it, then fix it with composition before reaching for context.
🎯 What you'll learn
- Components as the unit of reuse
- Props are read-only; children is just a prop
- Destructuring, defaults and composition over configuration
- Prop drilling and props on the fiber
📚 Before you start
1Components Are the Unit of Reuse
A component is a function that takes a single argument — props — and returns what to render. Everything reusable in a React app is built by composing components, the same way everything reusable in ordinary JS is built by composing functions.
type ButtonProps = { label: string; onClick: () => void };
function Button({ label, onClick }: ButtonProps) {
return <button onClick={onClick}>{label}</button>;
}
// reused three times with different props:
<Button label="Save" onClick={handleSave} />
<Button label="Cancel" onClick={handleCancel} />
<Button label="Delete" onClick={handleDelete} />2Props Are Read-Only
props is the data a parent passes down to a child. React enforces — by convention and by TypeScript typing, not by runtime freezing — that a component never mutates its own props. If a child needs to change something, the parent hands it a callback (like onClick above) that the child invokes; the parent decides how to respond.
type CounterDisplayProps = { count: number };
function CounterDisplay({ count }: CounterDisplayProps) {
// count += 1; ❌ never do this — props flow one direction: parent → child
return <p>Count: {count}</p>;
}This one-directional data flow is what makes React apps traceable: to find out why something rendered a certain way, you only ever have to look "upward" toward whoever passed the props, never guess who else might have silently mutated them.
3children Is Just a Prop
Anything nested between a component's open/close tags is passed as the special children prop:
function Card({ children }: { children: React.ReactNode }) {
return <div className="card">{children}</div>;
}
<Card>
<h2>Title</h2>
<p>Body text</p>
</Card>children is how React implements composition/slotting without a separate "template" concept — it's ordinary JSX passed as an ordinary prop, and can be any renderable value (elements, strings, arrays, null).
4Destructuring Props and Default Values
type AvatarProps = { src: string; size?: number };
function Avatar({ src, size = 40 }: AvatarProps) {
return <img src={src} width={size} height={size} />;
}
<Avatar src="/me.png" /> {/* size defaults to 40 */}
<Avatar src="/me.png" size={80} />Destructuring in the parameter list is the idiomatic way to both extract needed fields and declare defaults for optional props in one place.
5Composition Over Configuration
A common beginner mistake is adding more and more boolean/config props to a single component instead of composing smaller components:
// Fragile: growing prop surface for every variant
function Panel({ title, hasFooter, footerText, hasIcon, iconName }: PanelProps) { /* ... */ }
// Better: compose from smaller pieces via children
function Panel({ children }: { children: React.ReactNode }) {
return <div className="panel">{children}</div>;
}
function PanelHeader({ children }: { children: React.ReactNode }) {
return <div className="panel-header">{children}</div>;
}
<Panel>
<PanelHeader>Settings</PanelHeader>
{/* ...whatever content, however arranged... */}
</Panel>Composition scales better: new variants are built by rearranging existing pieces, not by adding more conditional branches to one component.
6Prop Drilling and Why It's a Smell (Not a Bug)
Passing a prop through several layers of components that don't use it themselves, just to reach a deeply nested child, is called prop drilling:
function App() { return <Page user={user} />; }
function Page({ user }: { user: User }) { return <Sidebar user={user} />; }
function Sidebar({ user }: { user: User }) { return <UserBadge user={user} />; } // only THIS needs itIt's not wrong — it's explicit and traceable — but past 2-3 levels it gets tedious and couples every intermediate component to data it doesn't care about. The useContext and "State management patterns" topics cover the standard fixes.
Under the HoodProps Are Just the Fiber's pendingProps/memoizedProps▾
Where props actually live
When React creates or updates a fiber for a component, the new props object from the latest render is stored on that fiber as pendingProps. Once the work for that fiber completes, React copies it to memoizedProps — the props the component was last actually rendered with. Your component function's parameter, at call time, is quite literally workInProgressFiber.pendingProps.
Fiber node (simplified)
├─ type: Button (the function/tag this fiber represents)
├─ pendingProps: {...} (props for the render currently in progress)
├─ memoizedProps: {...} (props from the last COMPLETED render)
├─ memoizedState: ... (hooks linked list — see the UseState topic)
├─ child / sibling / return (fiber tree pointers)Why this is how React decides to skip re-rendering a subtree
React.memo (covered fully in "Performance optimization") works by comparing the new pendingProps object against the fiber's stored memoizedProps, field by field, using Object.is (reference equality for objects/arrays/functions, value equality for primitives). If every field is reference-equal, React can skip calling the component function entirely and reuse the existing fiber's subtree — this is only possible because props are a distinct, comparable object attached to each fiber, not because React re-derives them some other way.
Why passing a new object/array/function literal as a prop defeats this
// A new {} object literal is created on every parent render —
// even if its contents are identical, it's a different reference.
<Child style={{ color: "red" }} />memoizedProps.style !== pendingProps.style every single time (two different object references, Object.is returns false), so React.memo on Child never actually short-circuits — this is the single most common reason React.memo "doesn't seem to work," and it's a direct, mechanical consequence of how the comparison is implemented, not a bug.
⚠️ Common Pitfalls
- Mutating props directly — even though nothing stops you at runtime for plain objects, it breaks the traceability the whole model depends on, and can cause the parent and child to disagree about state.
- Forgetting
childrenneeds a type —React.ReactNodecovers all valid renderable children; a narrower type likestringwill reject valid JSX children. - New object/array/function literals as props to a
memo-wrapped child — defeats the reference-equality check described above; wrap withuseMemo/useCallbackif the child is memoized and this matters. - Excessive prop drilling instead of
useContextor composition — a sign the component tree's shape doesn't match the data's actual scope. - Spreading unknown props onto DOM elements (
<div {...rest} />) without filtering — can leak component-only props (like a customvariantprop) onto the DOM element, causing React "unknown prop" warnings.
📋 Quick Reference
| Concept | Rule |
|---|---|
| Props flow | Parent → child, one direction, read-only |
| Changing child behavior | Parent passes a callback prop; child calls it |
children | Just a prop, populated by nested JSX |
| Optional props | prop?: T in the type + a default in destructuring |
| Fiber storage | pendingProps (this render) vs memoizedProps (last completed render) |
React.memo skip check | Object.is per prop field against memoizedProps |
🧠 Key takeaways
- Components are functions of props; props are read-only.
children(and other element props) enable composition: slot-style layouts.- Prefer composition over configuration (many boolean props).
- Prop drilling is a smell, not a bug: fix with composition first, context when data is truly global.
🎤 Interview Q&A
Q1What are props and how do they differ from state?core▾
Props are read-only inputs passed from parent to child; state is data owned and updated by a component. Changing props or state triggers re-renders.
- Can a child modify props? No; it calls a callback prop to ask the parent to change state.
Q2What is prop drilling and how do you avoid it?core▾
Passing props through many intermediate components that don't use them. Alternatives: composition (passing components as children), context for truly global data, or state management libraries.
- Why is composition often the first fix? It avoids context re-render costs and keeps data flow explicit.
Q3Controlled vs uncontrolled components?core▾
Controlled: form values live in React state and update via onChange; easy validation and conditional logic. Uncontrolled: the DOM holds values, read via refs or FormData; less code, better for simple forms or integrating non-React code.
- What does react-hook-form use? Mostly uncontrolled inputs for performance.
Q4How do you design reusable components?senior▾
Small focused components, clear typed props, composition via children/slots, sensible defaults, forwarding refs and rest props to DOM elements, and separating presentational components from data fetching.
- What's the compound component pattern? Components that work together via shared context, like
<Tabs><Tabs.List/>…</Tabs>.
Q5What are children and render props?core▾
children is the content between a component's tags, enabling composition. Render props pass a function that returns UI, letting a component share behaviour while the caller controls rendering; hooks replaced most uses.
- When are render props still useful? When the shared logic must control where UI renders, e.g. virtualized lists.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.
✅ Check yourself
Answer the questions — feedback is instant.