Props — Passing Data Between Components

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>
  );
}
Note: Props are read-only. A component must never modify its props. This is a core React rule — components are pure functions of their props and state. If a component needs to transform prop data (e.g. format a date), compute that value in a variable inside the component body. If it needs to change the data in the parent, it must call a callback function passed as a prop.
Tip: Pass the entire object as a single prop (<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.
Warning: Avoid passing too many unrelated props to a single component. If a component receives more than 5–6 props it may be doing too many things. Group related props into objects — a 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}>

🧠 Test Yourself

A child component calls props.onDelete(post._id) when the user clicks a button. The parent does not pass an onDelete prop. What happens and how do you fix it?