Components, Props, and Children
Components Are Functions. That's It.
Strip away the abstractions and a React component is a JavaScript function that accepts an object (props) and returns React elements. Everything else — hooks, effects, context — layers on top of this core idea.
// This is a complete React component:
function Greeting({ name }) {
return <h1>Hello, {name}</h1>;
}
// Which is identical to:
function Greeting(props) {
return React.createElement('h1', null, 'Hello, ', props.name);
}
When React encounters <Greeting name="Alice" /> in the tree, it calls Greeting({ name: 'Alice' }) and uses the returned elements to build the UI.
Think of a component as a recipe. Props are the ingredients you hand to the recipe. The return value is the dish (React elements). Every time ingredients change, React calls the recipe again to get a fresh dish. The recipe never modifies the ingredients — it reads them, uses them, and produces output. If you want different output, pass different ingredients.
Props Are Read-Only
This is not a suggestion. It is a rule React enforces conceptually and that TypeScript enforces in practice:
function UserCard({ user }) {
// NEVER DO THIS:
// user.name = 'Modified'; // Mutating props breaks React's model
return <div>{user.name}</div>;
}
React assumes that if props have not changed (by reference), the component output has not changed. Mutating props violates this contract and causes bugs that are nearly impossible to trace — stale renders, missed updates, and incorrect memoization.
Why props immutability matters for performance
React.memo, useMemo, and the upcoming React Compiler all rely on referential equality checks to skip unnecessary work. If prevProps.user === nextProps.user, React can skip rendering. But if you mutate user.name directly, the reference stays the same (=== returns true) while the data has changed. React sees "same reference" and skips the render — showing stale data. Immutable props make equality checks reliable.
Children: The Most Misunderstood Prop
children is not special syntax. It is a regular prop that JSX passes automatically for nested content:
// These two are identical:
<Card>
<p>Hello</p>
</Card>
<Card children={<p>Hello</p>} />
The compiler transforms both into createElement(Card, { children: createElement('p', null, 'Hello') }).
Children Can Be Anything
// String
<Button>Click me</Button>
// Element
<Layout><Sidebar /></Layout>
// Array of elements
<List>
<Item />
<Item />
<Item />
</List>
// Function (render prop pattern)
<DataFetcher>
{(data) => <Chart data={data} />}
</DataFetcher>
// Nothing
<EmptyWrapper></EmptyWrapper> // children is undefined
Working with Children
React provides utilities for working with children because they can be a single element, an array, or undefined:
import { Children, cloneElement } from 'react';
function RadioGroup({ children, value, onChange }) {
return (
<div role="radiogroup">
{Children.map(children, (child) =>
cloneElement(child, {
checked: child.props.value === value,
onChange,
})
)}
</div>
);
}
Children.count(children) and children.length give different results. If children is a single element, children.length is undefined (it is not an array). Children.count handles all cases — single element, array, null, fragment. Always use React.Children utilities when you need to inspect or iterate children.
Composition Patterns
Containment (Slots)
Components that do not know their children ahead of time use props as slots:
function PageLayout({ header, sidebar, children }) {
return (
<div className="page">
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}
// Usage:
<PageLayout
header={<NavBar />}
sidebar={<CourseMenu />}
>
<LessonContent />
</PageLayout>
This is more flexible than inheritance. Each slot accepts any React element — the parent component does not need to know what goes in each slot.
Specialization
A generic component configured for a specific use case:
function Dialog({ title, message, type = 'info', onClose }) {
return (
<div className={`dialog dialog--${type}`}>
<h2>{title}</h2>
<p>{message}</p>
<button onClick={onClose}>Close</button>
</div>
);
}
// Specialized version:
function DeleteConfirmation({ itemName, onConfirm, onCancel }) {
return (
<Dialog
title="Confirm Delete"
message={`Are you sure you want to delete "${itemName}"?`}
type="danger"
onClose={onCancel}
/>
);
}
Component as Prop
Pass entire components (not elements) as props for maximum flexibility:
function List({ items, renderItem }) {
return (
<ul>
{items.map((item, index) => (
<li key={item.id}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// Usage:
<List
items={users}
renderItem={(user) => <UserCard user={user} />}
/>
Production Scenario: Building a Polymorphic Button
A production component that renders as different elements based on props:
type ButtonProps = {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
as?: 'button' | 'a' | 'link';
href?: string;
children: React.ReactNode;
onClick?: () => void;
};
function Button({
variant = 'primary',
size = 'md',
as = 'button',
href,
children,
onClick,
}: ButtonProps) {
const className = `btn btn--${variant} btn--${size}`;
if (as === 'a' || href) {
return (
<a href={href} className={className} onClick={onClick}>
{children}
</a>
);
}
return (
<button className={className} onClick={onClick}>
{children}
</button>
);
}
-
Wrong: Mutating props inside a component Right: Treat props as read-only. Create new objects or use state for local changes.
-
Wrong: Using children.length to count children Right: Use React.Children.count(children)
-
Wrong: Deeply nesting components for code reuse (inheritance thinking) Right: Use composition — containment, specialization, render props
-
Wrong: Defining components inside other components Right: Define components at the top level of the module
- 1Components are functions: props in, elements out. Every render is a fresh function call.
- 2Props are immutable — never modify them. Create new objects or use state instead.
- 3children is a regular prop, not magic syntax. It can be strings, elements, arrays, functions, or undefined.
- 4Composition over inheritance — use containment (slots), specialization, and render props.
- 5Never define components inside other components — it causes unmount/remount on every render.