Components — Props and Composition Patterns
Function Component Basics
A React component is a pure function that accepts props and returns JSX. The function name must start with an uppercase letter(lowercase names are treated as HTML tags).
// ✅ Correct component definition
function Greeting() {
return <h1>Hello!</h1>;
}
// ✅ Arrow functions work too
const Greeting = () => <h1>Hello!</h1>;
// ❌ Lowercase start — treated as an HTML tag
function greeting() {
return <h1>Hello!</h1>;
}
Passing and Receiving Props
Props (Properties) are the mechanism for passing data from a parent component to a child component.
// Passing props (parent)
function App() {
return (
<UserCard
name="John Smith"
age={28}
isAdmin={true}
hobbies={['coding', 'reading', 'exercise']}
address={{ city: 'New York', district: 'Manhattan' }}
/>
);
}
// Receiving props (child)
function UserCard({ name, age, isAdmin, hobbies, address }) {
return (
<div className="user-card">
<h2>{name} {isAdmin && '👑'}</h2>
<p>Age: {age}</p>
<p>Location: {address.city}, {address.district}</p>
<ul>
{hobbies.map((hobby, i) => (
<li key={i}>{hobby}</li>
))}
</ul>
</div>
);
}
Props are Read-Only
// ❌ Never mutate props directly
function Counter({ count }) {
count++; // Never do this!
return <p>{count}</p>;
}
// ✅ Copy to a local variable or use state if needed
function Counter({ initialCount }) {
const [count, setCount] = React.useState(initialCount);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
The children Prop
children is a special prop that holds whatever content is placed between a component's tags.
// Basic children
function Card({ title, children }) {
return (
<div className="card">
<h3 className="card-title">{title}</h3>
<div className="card-body">
{children}
</div>
</div>
);
}
// Usage
function App() {
return (
<Card title="Announcement">
<p>Server maintenance is scheduled for today.</p>
<a href="/notice">Read more</a>
</Card>
);
}
Handling children Types
import { Children, isValidElement } from 'react';
function List({ children }) {
// Manipulate children with the Children utility
const count = Children.count(children);
const items = Children.toArray(children);
return (
<div>
<p>Item count: {count}</p>
<ul>
{items.map((child, index) => (
<li key={index}>{child}</li>
))}
</ul>
</div>
);
}
Component Composition Patterns
React recommends composition over inheritance.
Slot Pattern
// Layout component with multiple slots
function PageLayout({ header, sidebar, children, footer }) {
return (
<div className="layout">
<header>{header}</header>
<div className="content">
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
<footer>{footer}</footer>
</div>
);
}
// Usage
function App() {
return (
<PageLayout
header={<NavBar />}
sidebar={<SideMenu />}
footer={<Footer />}
>
<ArticleList />
</PageLayout>
);
}
Wrapper Components
// Wrapper component that adds functionality
function ErrorBoundaryWrapper({ children }) {
return (
<ErrorBoundary fallback={<ErrorPage />}>
<Suspense fallback={<Loading />}>
{children}
</Suspense>
</ErrorBoundary>
);
}
// Conditional access wrapper
function PrivateRoute({ children, requiredRole }) {
const { user } = useAuth();
if (!user) return <Navigate to="/login" />;
if (requiredRole && !user.roles.includes(requiredRole)) {
return <AccessDenied />;
}
return children;
}
Default Prop Values
Default Parameters (Recommended)
function Button({ label, variant = 'primary', size = 'medium', disabled = false }) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
>
{label}
</button>
);
}
// Usage
<Button label="Click" /> // Uses default values
<Button label="Delete" variant="danger" /> // Only variant changed
<Button label="Save" variant="success" size="large" />
defaultProps (Legacy — avoid with function components)
// ⚠️ defaultProps is heading toward deprecation for function components
function Avatar({ src, alt, size }) {
return <img src={src} alt={alt} width={size} height={size} />;
}
// Old way
Avatar.defaultProps = {
alt: 'User avatar',
size: 40,
};
// ✅ Recommended: use default parameters
function Avatar({ src, alt = 'User avatar', size = 40 }) {
return <img src={src} alt={alt} width={size} height={size} />;
}
Props Spread
// Pass through props with spread
function Input({ label, id, ...rest }) {
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} {...rest} />
</div>
);
}
// Usage
<Input
label="Email"
id="email"
type="email"
placeholder="example@email.com"
required
maxLength={100}
/>
Practical Example: UI Component Library Structure
// Button component (reusable UI primitive)
function Button({
children,
variant = 'primary',
size = 'md',
loading = false,
leftIcon,
rightIcon,
fullWidth = false,
onClick,
...rest
}) {
const sizeClasses = {
sm: 'px-3 py-1 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
danger: 'bg-red-600 text-white hover:bg-red-700',
ghost: 'bg-transparent border border-gray-300 hover:bg-gray-50',
};
return (
<button
className={[
'rounded font-medium transition-colors',
sizeClasses[size],
variantClasses[variant],
fullWidth ? 'w-full' : '',
loading ? 'opacity-70 cursor-not-allowed' : '',
].join(' ')}
disabled={loading}
onClick={onClick}
{...rest}
>
{loading && <span className="spinner mr-2" />}
{leftIcon && <span className="mr-2">{leftIcon}</span>}
{children}
{rightIcon && <span className="ml-2">{rightIcon}</span>}
</button>
);
}
// Modal component (using composition pattern)
function Modal({ isOpen, onClose, title, children, footer }) {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2>{title}</h2>
<button onClick={onClose} aria-label="Close">✕</button>
</div>
<div className="modal-body">{children}</div>
{footer && <div className="modal-footer">{footer}</div>}
</div>
</div>
);
}
// Usage example
function DeleteConfirmModal({ isOpen, onClose, onConfirm, itemName }) {
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title="Confirm Delete"
footer={
<>
<Button variant="ghost" onClick={onClose}>Cancel</Button>
<Button variant="danger" onClick={onConfirm}>Delete</Button>
</>
}
>
<p>Are you sure you want to delete <strong>{itemName}</strong>?</p>
<p className="text-red-600">This action cannot be undone.</p>
</Modal>
);
}
Pro Tips
1. When to split a component
Split a component when you see these signs:
- The same JSX block is repeated in two or more places
- Rendering logic becomes too complex (over 200 lines)
- A unit that you want to test independently emerges
2. Preventing Props Drilling
If you need to pass props more than 3 levels deep, consider using Context or a state management library.
// ❌ Props Drilling
<App user={user}>
<Layout user={user}>
<Sidebar user={user}>
<UserMenu user={user} />
</Sidebar>
</Layout>
</App>
// ✅ Solved with Context
const UserContext = createContext(null);
<UserContext.Provider value={user}>
<Layout>
<Sidebar>
<UserMenu /> {/* Access directly from Context */}
</Sidebar>
</Layout>
</UserContext.Provider>
3. Type Documentation (JSDoc)
Even without TypeScript, documenting props with JSDoc enables IDE autocompletion.
/**
* @param {{ name: string, role: 'admin' | 'user', onLogout: () => void }} props
*/
function UserBadge({ name, role, onLogout }) {
return (
<div>
<span>{name}</span>
<span className={`role-${role}`}>{role}</span>
<button onClick={onLogout}>Logout</button>
</div>
);
}