Props (short for properties) are the mechanism React uses to pass data from a parent component to a child component. Every piece of data a component needs to render — a post’s title, an author’s name, a loading flag, a click handler — arrives as a prop. Understanding how to pass, receive, destructure, and type-check props is the most fundamental skill in React component development. In this lesson you will learn every aspect of the props system and apply it throughout the MERN Blog component library.
How Props Work
// Parent passes props
function HomePage() {
const post = { _id: '1', title: 'MERN Tutorial', viewCount: 142 };
return <PostCard post={post} featured={true} onDelete={handleDelete} />;
}
// Child receives props as a single object
function PostCard(props) {
// { post: { _id: '1', ... }, featured: true, onDelete: [Function] }
return <div>{props.post.title}</div>;
}
// Destructure props for cleaner code (standard practice)
function PostCard({ post, featured, onDelete }) {
return (
<article>
<h2>{post.title}</h2>
{featured && <span>⭐ Featured</span>}
<button onClick={() => onDelete(post._id)}>Delete</button>
</article>
);
}
<PostCard post={post} />) rather than spreading all its fields individually. The object approach means you only need to update the PostCard component signature once if you add new fields to a post — not every call site in the codebase.post object is better than separate title, body, author, and viewCount props.Every Prop Type You Can Pass
function DemoParent() {
const post = { _id: '1', title: 'MERN Tutorial' };
const handleFn = (id) => console.log('clicked', id);
return (
<DemoChild
title="Hello World" // String
viewCount={142} // Number (curly braces, no quotes)
featured // Boolean shorthand for true
unpublished={false} // Boolean false
post={post} // Object
tags={['mern', 'react']} // Array
onDelete={handleFn} // Function (callback)
icon={<span>🌟</span>} // JSX element
coverImage={null} // null
/>
);
}
Default Prop Values
// Default values in destructuring (modern, preferred approach)
function PostCard({
post,
featured = false,
showActions = true,
variant = 'standard',
onDelete = () => {}, // no-op prevents "onDelete is not a function" errors
}) {
return (
<article className={`post-card post-card--${variant}`}>
<h2>{post.title}</h2>
{featured && <span>⭐ Featured</span>}
{showActions && (
<button onClick={() => onDelete(post._id)}>Delete</button>
)}
</article>
);
}
Callback Props — Child Communicating to Parent
// Data flows DOWN as props, events flow UP as callbacks
// Parent owns state and logic
function PostListPage() {
const [posts, setPosts] = useState([]);
const handleDelete = async (postId) => {
await deletePost(postId); // call Express API
setPosts(prev => prev.filter(p => p._id !== postId));
};
return (
<div>
{posts.map(post => (
<PostCard
key={post._id}
post={post}
onDelete={handleDelete} // callback passed down
/>
))}
</div>
);
}
// Child calls the callback — does not know about state
function PostCard({ post, onDelete }) {
return (
<article>
<h2>{post.title}</h2>
<button onClick={() => onDelete(post._id)}>Delete</button>
</article>
);
}
Spreading Props
// Useful for wrapper components that forward props to a native element
function Button({ children, variant = 'primary', ...rest }) {
// ...rest captures all remaining props (onClick, disabled, type, etc.)
return (
<button className={`btn btn--${variant}`} {...rest}>
{children}
</button>
);
}
// Extra props forwarded automatically
<Button onClick={handleSubmit} disabled={isLoading} type="submit">
{isLoading ? 'Saving...' : 'Save Post'}
</Button>
Common Mistakes
Mistake 1 — Mutating props directly
❌ Wrong — modifying a prop object causes subtle bugs:
function PostCard({ post }) {
post.title = post.title.toUpperCase(); // mutates the parent's object!
}
✅ Correct — compute derived values, never mutate props:
function PostCard({ post }) {
const displayTitle = post.title.toUpperCase(); // new variable ✓
return <h2>{displayTitle}</h2>;
}
Mistake 2 — Not providing a default for optional callback props
❌ Wrong — crashes if parent does not pass the callback:
function PostCard({ post, onDelete }) {
return <button onClick={() => onDelete(post._id)}>Delete</button>;
// TypeError: onDelete is not a function (when not provided)
}
✅ Correct — default to a no-op function:
function PostCard({ post, onDelete = () => {} }) { // ✓
return <button onClick={() => onDelete(post._id)}>Delete</button>;
}
Mistake 3 — Passing a number as a string
❌ Wrong — string concatenation instead of arithmetic:
<Counter count="5" /> // "5" is a string
// Inside Counter: "5" + 1 === "51" not 6!
✅ Correct — use curly braces for non-string values:
<Counter count={5} /> // 5 is a number ✓
Quick Reference
| Task | Code |
|---|---|
| Pass string | <C title="Hello" /> |
| Pass number | <C count={5} /> |
| Pass boolean true | <C featured /> |
| Pass object | <C post={postObj} /> |
| Pass callback | <C onDelete={handleDelete} /> |
| Destructure props | function C({ title, count }) { ... } |
| Default value | function C({ size = 40 }) { ... } |
| Rest props | function C({ label, ...rest }) { ... } |
| Spread rest | <button {...rest}> |