Props and PropTypes
Learn how to pass data between components using props and validate them with PropTypes for robust, reusable components.
Learning Objectives
- Understand what props are and how they work
- Learn different ways to pass and receive props
- Master props destructuring and default values
- Validate props with PropTypes for better development
- Build reusable components with flexible APIs
- Handle complex prop patterns and edge cases
Props and PropTypes
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.
What are Props?
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.
Props in Action
// 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> ); }
Ways to Receive Props
1. Props Object (Traditional Way)
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" />
2. Destructuring Props (Modern Way)
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> ); }
3. Destructuring with Default Values
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 />
Types of Props
1. String Props
function Button({ text, variant }) { return ( <button className={`btn btn-${variant}`}> {text} </button> ); } // Usage <Button text="Click me" variant="primary" /> <Button text="Cancel" variant="secondary" />
2. Number Props
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} />
3. Boolean Props
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} />
4. Array Props
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" />
5. Object Props
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} />
6. Function Props (Event Handlers)
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> ); }
Advanced Props Patterns
1. Rest Props and Spread
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" />
2. Children Prop
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>
3. Render Props Pattern
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>
4. Compound Components
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 for Type Checking
PropTypes help you catch bugs by validating the types of props your components receive:
Installing PropTypes
npm install prop-types
Basic PropTypes
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' };
Advanced PropTypes
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: {} };
Complex PropTypes Examples
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 };
Real-World Examples
1. Complete User Profile Component
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 };
2. Flexible Button Component
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>
Common Props Mistakes and Solutions
1. Mutating Props
❌ 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> ); }
2. Not Validating Required Props
❌ 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 };
3. Inefficient Prop Passing
❌ 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>; }
Best Practices
1. Use Descriptive Prop Names
❌ Bad:
<Button type="1" click={handleClick} txt="Submit" />
✅ Good:
<Button variant="primary" onClick={handleClick} children="Submit" />
2. Group Related Props into Objects
❌ 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" }} />
3. Use Default Props Wisely
✅ 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']) };
Summary
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
What's Next?
In the next lesson, we'll explore Component Composition - advanced patterns for building complex UIs by combining simple components. You'll learn:
- Higher-order components (HOCs)
- Render props patterns
- Compound components
- Composition vs inheritance
- Building flexible component APIs
Props enable data flow, but composition enables architectural flexibility!