Learn how to pass data between components using props and validate them with PropTypes for robust, reusable components.
Props (short for "properties") are how you pass data from parent components to child components in React. They're the primary way to make components reusable and dynamic. Think of props as function arguments for your components.
Props are read-only data passed from a parent component to a child component. They allow you to customize how a component behaves and what it displays.
// Parent component passing props
function App() {
return (
<div>
<Greeting name="Alice" age={25} />
<Greeting name="Bob" age={30} />
<Greeting name="Charlie" age={35} />
</div>
);
}
// Child component receiving props
function Greeting(props) {
return (
<div>
<h1>Hello, {props.name}!</h1>
<p>You are {props.age} years old.</p>
</div>
);
}
function UserCard(props) {
return (
<div className="user-card">
<img src={props.avatar} alt={props.name} />
<h2>{props.name}</h2>
<p>{props.email}</p>
<span className="role">{props.role}</span>
</div>
);
}
// Usage
<UserCard
name="Sarah Johnson"
email="sarah@example.com"
avatar="sarah.jpg"
role="Frontend Developer"
/>
function UserCard({ name, email, avatar, role }) {
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h2>{name}</h2>
<p>{email}</p>
<span className="role">{role}</span>
</div>
);
}
function UserCard({
name,
email,
avatar = '/default-avatar.png', // Default value
role = 'User', // Default value
isOnline = false // Default value
}) {
return (
<div className="user-card">
<div className="avatar-container">
<img src={avatar} alt={name} />
{isOnline && <div className="online-indicator" />}
</div>
<h2>{name}</h2>
<p>{email}</p>
<span className="role">{role}</span>
</div>
);
}
// Usage - some props will use defaults
<UserCard
name="John Doe"
email="john@example.com"
isOnline={true}
// avatar and role will use default values
/>
function Button({ text, variant }) {
return (
<button className={`btn btn-\${variant}`}>
{text}
</button>
);
}
// Usage
<Button text="Click me" variant="primary" />
<Button text="Cancel" variant="secondary" />
function ProgressBar({ progress, max = 100 }) {
const percentage = (progress / max) * 100;
return (
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `\${percentage}%` }}
/>
<span className="progress-text">
{progress}/{max} ({Math.round(percentage)}%)
</span>
</div>
);
}
// Usage
<ProgressBar progress={75} max={100} />
<ProgressBar progress={3} max={10} />
function Alert({ message, isVisible, isError = false, isDismissible = true }) {
if (!isVisible) return null;
return (
<div className={`alert \${isError ? 'alert-error' : 'alert-info'}`}>
<span>{message}</span>
{isDismissible && (
<button className="alert-close">×</button>
)}
</div>
);
}
// Usage
<Alert
message="Success! Your data has been saved."
isVisible={true}
isError={false}
/>
<Alert
message="Error! Something went wrong."
isVisible={true}
isError={true}
isDismissible={false}
/>
function TagList({ tags, colorScheme = 'blue' }) {
return (
<div className="tag-list">
{tags.map((tag, index) => (
<span
key={index}
className={`tag tag-\${colorScheme}`}
>
{tag}
</span>
))}
</div>
);
}
// Usage
<TagList
tags={['React', 'JavaScript', 'Frontend']}
colorScheme="green"
/>
function ProductCard({ product }) {
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p className="price">\${product.price}</p>
<p className="description">{product.description}</p>
<div className="product-meta">
<span className="category">{product.category}</span>
<span className="rating">★ {product.rating}</span>
</div>
<button className="add-to-cart">
Add to Cart
</button>
</div>
);
}
// Usage
const product = {
id: 1,
name: "Wireless Headphones",
price: 99.99,
description: "High-quality wireless headphones with noise cancellation",
image: "headphones.jpg",
category: "Electronics",
rating: 4.5
};
<ProductCard product={product} />
function Modal({ isOpen, title, children, onClose, onConfirm }) {
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} className="close-btn">×</button>
</div>
<div className="modal-body">
{children}
</div>
<div className="modal-footer">
<button onClick={onClose} className="btn-secondary">
Cancel
</button>
<button onClick={onConfirm} className="btn-primary">
Confirm
</button>
</div>
</div>
</div>
);
}
// Usage
function App() {
const [showModal, setShowModal] = useState(false);
const handleConfirm = () => {
console.log('User confirmed!');
setShowModal(false);
};
return (
<div>
<button onClick={() => setShowModal(true)}>
Open Modal
</button>
<Modal
isOpen={showModal}
title="Confirm Action"
onClose={() => setShowModal(false)}
onConfirm={handleConfirm}
>
<p>Are you sure you want to proceed?</p>
</Modal>
</div>
);
}
function CustomInput({ label, error, ...inputProps }) {
return (
<div className="form-field">
{label && <label className="form-label">{label}</label>}
<input
className={`form-input \${error ? 'form-input-error' : ''}`}
{...inputProps} // Spread remaining props to input
/>
{error && <span className="form-error">{error}</span>}
</div>
);
}
// Usage - all props except label and error go to input
<CustomInput
label="Email Address"
type="email"
placeholder="Enter your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
autoComplete="email"
/>
The children prop is special - it represents content between component tags:
function Card({ title, children, footer }) {
return (
<div className="card">
{title && (
<div className="card-header">
<h3>{title}</h3>
</div>
)}
<div className="card-body">
{children} {/* Content between <Card></Card> tags */}
</div>
{footer && (
<div className="card-footer">
{footer}
</div>
)}
</div>
);
}
// Usage
<Card title="User Profile" footer={<button>Edit</button>}>
<img src="avatar.jpg" alt="User" />
<h4>John Doe</h4>
<p>Software Engineer</p>
<p>john@example.com</p>
</Card>
function DataFetcher({ url, children }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error.message);
setLoading(false);
});
}, [url]);
// Call children as a function with data
return children({ data, loading, error });
}
// Usage
<DataFetcher url="/api/users">
{({ data, loading, error }) => {
if (loading) return <div>Loading users...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}}
</DataFetcher>
function Tabs({ children, activeTab, onTabChange }) {
return (
<div className="tabs">
{children}
</div>
);
}
function TabList({ children }) {
return (
<div className="tab-list" role="tablist">
{children}
</div>
);
}
function Tab({ children, value, isActive, onClick }) {
return (
<button
className={`tab \${isActive ? 'tab-active' : ''}`}
onClick={() => onClick(value)}
role="tab"
>
{children}
</button>
);
}
function TabPanels({ children }) {
return (
<div className="tab-panels">
{children}
</div>
);
}
function TabPanel({ children, value, activeTab }) {
if (value !== activeTab) return null;
return (
<div className="tab-panel" role="tabpanel">
{children}
</div>
);
}
// Usage
function App() {
const [activeTab, setActiveTab] = useState('profile');
return (
<Tabs activeTab={activeTab} onTabChange={setActiveTab}>
<TabList>
<Tab value="profile" isActive={activeTab === 'profile'} onClick={setActiveTab}>
Profile
</Tab>
<Tab value="settings" isActive={activeTab === 'settings'} onClick={setActiveTab}>
Settings
</Tab>
<Tab value="billing" isActive={activeTab === 'billing'} onClick={setActiveTab}>
Billing
</Tab>
</TabList>
<TabPanels>
<TabPanel value="profile" activeTab={activeTab}>
<h2>User Profile</h2>
<p>Profile content goes here</p>
</TabPanel>
<TabPanel value="settings" activeTab={activeTab}>
<h2>Settings</h2>
<p>Settings content goes here</p>
</TabPanel>
<TabPanel value="billing" activeTab={activeTab}>
<h2>Billing</h2>
<p>Billing content goes here</p>
</TabPanel>
</TabPanels>
</Tabs>
);
}
PropTypes help you catch bugs by validating the types of props your components receive:
npm install prop-types
import PropTypes from 'prop-types';
function UserCard({ name, age, email, isActive, hobbies, avatar }) {
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h2>{name}</h2>
<p>Age: {age}</p>
<p>Email: {email}</p>
<p>Status: {isActive ? 'Active' : 'Inactive'}</p>
<div>
Hobbies: {hobbies.join(', ')}
</div>
</div>
);
}
UserCard.propTypes = {
name: PropTypes.string.isRequired, // Required string
age: PropTypes.number.isRequired, // Required number
email: PropTypes.string.isRequired, // Required string
isActive: PropTypes.bool, // Optional boolean
hobbies: PropTypes.array, // Optional array
avatar: PropTypes.string // Optional string
};
UserCard.defaultProps = {
isActive: false,
hobbies: [],
avatar: '/default-avatar.png'
};
import PropTypes from 'prop-types';
function ProductCard({ product, onAddToCart, tags, size, customStyle }) {
return (
<div className={`product-card product-card-\${size}`} style={customStyle}>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>\${product.price}</p>
<div className="tags">
{tags.map(tag => (
<span key={tag} className="tag">{tag}</span>
))}
</div>
<button onClick={() => onAddToCart(product.id)}>
Add to Cart
</button>
</div>
);
}
ProductCard.propTypes = {
// Object with specific shape
product: PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
image: PropTypes.string.isRequired
}).isRequired,
// Function
onAddToCart: PropTypes.func.isRequired,
// Array of specific type
tags: PropTypes.arrayOf(PropTypes.string),
// One of specific values
size: PropTypes.oneOf(['small', 'medium', 'large']),
// Object (CSS styles)
customStyle: PropTypes.object,
// Any type
metadata: PropTypes.any,
// Node (anything that can be rendered)
icon: PropTypes.node,
// Element (React element)
header: PropTypes.element,
// Instance of a class
date: PropTypes.instanceOf(Date),
// Custom validator
priority: function(props, propName, componentName) {
if (props[propName] && (props[propName] < 1 || props[propName] > 10)) {
return new Error(
`Invalid prop `\${propName}` of value `\${props[propName]}` supplied to `\${componentName}`, expected a number between 1 and 10.`
);
}
}
};
ProductCard.defaultProps = {
tags: [],
size: 'medium',
customStyle: {}
};
import PropTypes from 'prop-types';
function DataTable({ columns, data, onRowClick, sortable, pagination }) {
// Component implementation...
return <div>/* Table implementation */</div>;
}
DataTable.propTypes = {
// Array of objects with specific shape
columns: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
sortable: PropTypes.bool,
render: PropTypes.func
})
).isRequired,
// Array of any objects
data: PropTypes.arrayOf(PropTypes.object).isRequired,
// Function with specific signature
onRowClick: PropTypes.func,
// Object with optional properties
sortable: PropTypes.shape({
enabled: PropTypes.bool,
defaultSort: PropTypes.string,
direction: PropTypes.oneOf(['asc', 'desc'])
}),
// Union type - either boolean or object
pagination: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.shape({
pageSize: PropTypes.number,
currentPage: PropTypes.number,
totalItems: PropTypes.number
})
])
};
DataTable.defaultProps = {
sortable: {
enabled: true,
direction: 'asc'
},
pagination: false
};
import PropTypes from 'prop-types';
function UserProfile({
user,
onEdit,
onMessage,
onFollow,
isFollowing,
showActions = true,
compact = false
}) {
const {
id,
name,
username,
avatar,
bio,
location,
website,
joinDate,
stats,
isVerified,
isOnline
} = user;
return (
<div className={`user-profile \${compact ? 'user-profile-compact' : ''}`}>
<div className="user-header">
<div className="avatar-container">
<img src={avatar} alt={`\${name}'s avatar`} className="avatar" />
{isOnline && <div className="online-indicator" />}
</div>
<div className="user-info">
<div className="name-section">
<h1 className="name">{name}</h1>
{isVerified && <span className="verified-badge">✓</span>}
</div>
<p className="username">@{username}</p>
{!compact && (
<>
{bio && <p className="bio">{bio}</p>}
<div className="metadata">
{location && (
<span className="location">📍 {location}</span>
)}
{website && (
<a href={website} className="website" target="_blank" rel="noopener noreferrer">
🔗 {website}
</a>
)}
<span className="join-date">
📅 Joined {new Date(joinDate).toLocaleDateString()}
</span>
</div>
</>
)}
</div>
</div>
{!compact && stats && (
<div className="user-stats">
<div className="stat">
<span className="stat-number">{stats.posts.toLocaleString()}</span>
<span className="stat-label">Posts</span>
</div>
<div className="stat">
<span className="stat-number">{stats.followers.toLocaleString()}</span>
<span className="stat-label">Followers</span>
</div>
<div className="stat">
<span className="stat-number">{stats.following.toLocaleString()}</span>
<span className="stat-label">Following</span>
</div>
</div>
)}
{showActions && (
<div className="user-actions">
<button onClick={() => onEdit(id)} className="btn btn-secondary">
Edit Profile
</button>
<button onClick={() => onMessage(id)} className="btn btn-primary">
Message
</button>
<button
onClick={() => onFollow(id)}
className={`btn \${isFollowing ? 'btn-outline' : 'btn-primary'}`}
>
{isFollowing ? 'Unfollow' : 'Follow'}
</button>
</div>
)}
</div>
);
}
UserProfile.propTypes = {
user: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
avatar: PropTypes.string.isRequired,
bio: PropTypes.string,
location: PropTypes.string,
website: PropTypes.string,
joinDate: PropTypes.string.isRequired,
isVerified: PropTypes.bool,
isOnline: PropTypes.bool,
stats: PropTypes.shape({
posts: PropTypes.number,
followers: PropTypes.number,
following: PropTypes.number
})
}).isRequired,
onEdit: PropTypes.func.isRequired,
onMessage: PropTypes.func.isRequired,
onFollow: PropTypes.func.isRequired,
isFollowing: PropTypes.bool,
showActions: PropTypes.bool,
compact: PropTypes.bool
};
UserProfile.defaultProps = {
isFollowing: false,
showActions: true,
compact: false
};
import PropTypes from 'prop-types';
function Button({
children,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
fullWidth = false,
icon,
iconPosition = 'left',
onClick,
...props
}) {
const baseClasses = 'btn';
const variantClasses = `btn-\${variant}`;
const sizeClasses = `btn-\${size}`;
const stateClasses = [
disabled && 'btn-disabled',
loading && 'btn-loading',
fullWidth && 'btn-full-width'
].filter(Boolean).join(' ');
const buttonClasses = [baseClasses, variantClasses, sizeClasses, stateClasses]
.join(' ')
.trim();
return (
<button
className={buttonClasses}
disabled={disabled || loading}
onClick={onClick}
{...props}
>
{loading && <span className="btn-spinner" />}
{icon && iconPosition === 'left' && (
<span className="btn-icon btn-icon-left">{icon}</span>
)}
<span className="btn-text">{children}</span>
{icon && iconPosition === 'right' && (
<span className="btn-icon btn-icon-right">{icon}</span>
)}
</button>
);
}
Button.propTypes = {
children: PropTypes.node.isRequired,
variant: PropTypes.oneOf([
'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'
]),
size: PropTypes.oneOf(['small', 'medium', 'large']),
disabled: PropTypes.bool,
loading: PropTypes.bool,
fullWidth: PropTypes.bool,
icon: PropTypes.node,
iconPosition: PropTypes.oneOf(['left', 'right']),
onClick: PropTypes.func
};
// Usage examples
<Button variant="primary" size="large" onClick={handleSubmit}>
Submit Form
</Button>
<Button
variant="danger"
icon={<TrashIcon />}
iconPosition="left"
onClick={handleDelete}
>
Delete Item
</Button>
<Button
variant="secondary"
loading={isLoading}
disabled={!isValid}
fullWidth
>
{isLoading ? 'Processing...' : 'Save Changes'}
</Button>
❌ Wrong - Never mutate props:
function UserList({ users }) {
users.push({ id: 999, name: 'New User' }); // NEVER DO THIS
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
✅ Correct - Create new arrays/objects:
function UserList({ users, showNewUser = false }) {
const displayUsers = showNewUser
? [...users, { id: 999, name: 'New User' }] // Create new array
: users;
return (
<ul>
{displayUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
❌ Wrong - No validation:
function UserCard({ user }) {
return (
<div>
<h2>{user.name}</h2> {/* Will crash if user is undefined */}
<p>{user.email}</p>
</div>
);
}
✅ Correct - Validate and provide defaults:
import PropTypes from 'prop-types';
function UserCard({ user }) {
if (!user) {
return <div>No user data available</div>;
}
return (
<div>
<h2>{user.name || 'Unknown User'}</h2>
<p>{user.email || 'No email provided'}</p>
</div>
);
}
UserCard.propTypes = {
user: PropTypes.shape({
name: PropTypes.string,
email: PropTypes.string
}).isRequired
};
❌ Wrong - Prop drilling:
function App() {
const user = { name: 'John', email: 'john@example.com' };
return <Dashboard user={user} />;
}
function Dashboard({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <UserMenu user={user} />;
}
function UserMenu({ user }) {
return <div>{user.name}</div>;
}
✅ Better - Component composition:
function App() {
const user = { name: 'John', email: 'john@example.com' };
return (
<Dashboard>
<Sidebar>
<UserMenu user={user} />
</Sidebar>
</Dashboard>
);
}
function Dashboard({ children }) {
return <div className="dashboard">{children}</div>;
}
function Sidebar({ children }) {
return <div className="sidebar">{children}</div>;
}
❌ Bad:
<Button type="1" click={handleClick} txt="Submit" />
✅ Good:
<Button variant="primary" onClick={handleClick} children="Submit" />
❌ Bad - Too many props:
<UserCard
userName="John"
userEmail="john@example.com"
userAvatar="john.jpg"
userAge={30}
userLocation="NYC"
/>
✅ Good - Grouped props:
<UserCard user={{
name: "John",
email: "john@example.com",
avatar: "john.jpg",
age: 30,
location: "NYC"
}} />
✅ Good practices:
function Avatar({ src, alt, size = 'medium', shape = 'circle' }) {
return (
<img
src={src || '/default-avatar.png'}
alt={alt}
className={`avatar avatar-\${size} avatar-\${shape}`}
/>
);
}
Avatar.propTypes = {
src: PropTypes.string,
alt: PropTypes.string.isRequired, // Required for accessibility
size: PropTypes.oneOf(['small', 'medium', 'large']),
shape: PropTypes.oneOf(['circle', 'square'])
};
Props are the foundation of component reusability in React:
✅ Props pass data from parent to child components
✅ Use destructuring for cleaner code
✅ Provide default values for optional props
✅ Validate props with PropTypes in development
✅ Never mutate props - they are read-only
✅ Use composition to avoid prop drilling
In the next lesson, we'll explore Component Composition - advanced patterns for building complex UIs by combining simple components. You'll learn:
Props enable data flow, but composition enables architectural flexibility!