Lesson 1 of 8
Props: Passing Data In
"Components let you split the UI into independent, reusable pieces, and think about each piece in isolation." — React documentation
You already know how to write a function that takes arguments and returns a value. A React component is exactly that — a function that takes one argument and returns UI. That single argument is called props, and it is the entire mechanism by which a parent component hands data down to a child. Master props and you have the vocabulary for every conversation React components ever have with one another. It is also the first thing an interviewer probes, because how a candidate talks about props reveals whether they understand React's deepest rule: data flows one way, and children never reach back up to change what they were given.
A parent component has data. A child component needs it. How does the data get there — and why is a component forbidden from ever changing the data it receives?
Core Concepts
A component is a function; props is its parameter object. When you write <Greeting name="Ada" />, React calls your Greeting function and passes it a single object — { name: "Ada" }. That object is the props. Every JSX attribute you write becomes one key on that object. There is no magic here: rendering a component is calling a function with an argument, and props is that argument.
The syntax for passing props has exactly one rule worth memorising: string literals use quotes; everything else uses curly braces.
Look at the console: the attributes arrived as a plain object. name="Ada" is a string, so quotes are fine. But age={36} needs braces — without them, age="36" would pass the string "36", not the number. And isAdmin on its own is shorthand for isAdmin={true}, the same way an HTML boolean attribute works. The braces are a window from JSX back into JavaScript; anything you can put in a variable, you can put between them.
Reading props: the object, or destructuring
Inside the component you can reach into the props object (props.name), but idiomatic React destructures props right in the parameter list. It reads cleaner and documents, at a glance, exactly what the component consumes:
Both styles receive the identical object; destructuring simply names the fields you care about up front. From here on we will destructure by default, as nearly all React code does.
Props are a food order passed to the kitchen. The customer (parent) writes the ticket — "medium, no onions, extra cheese" — and hands it through the window. The kitchen (child) reads the ticket and cooks accordingly. What the kitchen must never do is scribble on the customer's ticket and hand it back changed. The ticket flows one way, in one direction, and stays exactly as it was written. That is props in a sentence.
Props are read-only — this is the rule that defines React
A component must never mutate its props. Props are immutable from the child's point of view: they are input, not scratch paper. Writing props.name = "Bob" (or props.items.push(x)) is a bug, even when it seems to "work."
Why so strict? Because React's entire model rests on one-way data flow: data flows down from parent to child, and only the owner of a piece of data may change it. If any child could rewrite its props, you could never look at a parent and know what its children are displaying — the data on screen would depend on hidden edits happening three levels deep. Keeping props read-only makes rendering predictable: the same props always produce the same UI, every time, which is what lets React re-render safely and what lets you reason about a component in isolation. (When a component genuinely needs data that changes over time, that data belongs to the component as state — the subject of Part III. The distinction is precise: props come from above and are read-only here; state is owned here and is the only thing this component may change.)
Early web frameworks embraced two-way binding: change the view and the data updates, change the data and the view updates — automatically, in both directions. It felt magical in demos and became a nightmare in large apps, where a single edit could ripple through the system with no clear origin. React's founders at Facebook made the opposite bet in 2013: data flows one way only, and props are read-only. That constraint looked limiting and turned out to be liberating — you can always answer "why does the screen show this?" by tracing props downward from a single source. Nearly every framework since has copied the idea.
Props carry any type — including functions
A prop can hold anything a JavaScript variable can: strings, numbers, booleans, objects, arrays, and — crucially — functions. Passing a function down is how a child sends information back up without ever mutating anything: the parent owns the data and the handler, and simply lends the child a button to press.
Click a button and watch the console. The child never touched fruits or invented its own data — it called the function the parent handed it. That callback pattern is the teaser for the next Part, Events: this is precisely how a child "talks back" while data flow stays strictly one-way.
Default values with destructuring
A parent may omit a prop. Rather than guard against undefined everywhere, give the prop a default value in the destructuring — plain JavaScript default-parameter syntax, which you already know:
The default fires only when the prop is undefined (i.e. not passed). Pass tone={null} explicitly and the default does not apply — null is a value.
Habits experienced React engineers apply without thinking:
• They destructure props in the signature. The parameter list becomes a live checklist of what the component needs — reviewers can read the contract in one line.
• They default at the destructure, not with scattered ifs. One tidy place, using ordinary JS defaults, instead of || checks sprinkled through the body.
• They treat every prop — and every object and array inside it — as frozen. Need a changed version? Build a new one ([...items, x]), never items.push(x).
• They name callback props onSomething and their handlers handleSomething. The convention makes data-in (nouns) vs. events-out (on…) obvious at a glance.
Typing props with TypeScript
You finished Volume 2, so type your props — it is the single highest-value habit in a real React + TypeScript codebase. Declare a type (or interface) describing the props object and annotate the parameter. The compiler now enforces the component's contract: forget a required prop, or pass a number where a string belongs, and you learn at the call site, not at runtime.
The ? marks a prop optional; the default in the destructure supplies its value when omitted. That pairing — optional in the type, defaulted in the signature — is the standard, self-documenting way to declare a prop the caller may skip.
The Academy renders a certificate with a component roughly like <Certificate studentName="Ada Okoro" course="React" score={92} issuedOn={date} />. The page that owns the data — fetched from Drizzle/MySQL — passes it straight down as props; the Certificate component is a pure function of those props and never fetches or mutates anything. That purity is exactly why it can be reused on the student's dashboard, on the public /verify/<code> page, and inside the PDF exporter without a single change. A component that only reads its props travels anywhere; one that reaches out and mutates is welded to one place.
Common Mistakes
- Mutating a prop.
props.count++oritems.push(x)inside a child. It corrupts the one-way flow and often does nothing visible — the parent's data is untouched. Read props; never write them. - Forgetting the curly braces.
age="36"passes the string"36"; you meantage={36}. Only string literals take quotes; every other type needs{}. - Confusing props with state. Props come from the parent and are read-only here. State is owned by this component and is the only thing it may change. Reaching for props when you mean state (or vice-versa) is the classic beginner tangle.
- Passing a called function instead of the function.
onClick={handle()}runshandleduring render and passes its return value. You almost always wantonClick={handle}oronClick={() => handle(arg)}. - Relying on
||for defaults.count || 10turns a legitimate0into10. Use destructuring defaults ({ count = 10 }), which trigger only onundefined. - Spreading props blindly.
<Child {...everything} />is convenient but hides the contract and can forward props the child shouldn't get. Prefer naming what you pass.
A parent passes the same array object as a prop to two children: <A items={list} /> and <B items={list} />. Inside A, someone writes items.push("x"). What does B now render? Because both children received a reference to the same object, A has silently mutated the data B depends on — and worse, it changed the parent's own list. Nothing threw an error; the bug simply teleported across the component tree. This is the precise disaster one-way data flow and read-only props exist to prevent. Treat every prop as frozen and the scenario becomes impossible.
Interview Corner
name="Ada", and everything else in braces like age={36}. The child reads them from its props parameter, usually by destructuring."children prop so the value is injected where it's needed; or, for genuinely global data like theme or the current user, reach for Context. I'd resist jumping to Context prematurely — it trades traceability for convenience, so I use it for truly cross-cutting concerns, not to avoid typing a few props."- In the editor above, take the
ProductCardcomponent and add acurrencyprop that defaults to"$"via destructuring. Render two cards — one that passescurrency="₦"and one that omits it — and confirm the default fires only when the prop is absent. - Add a
PropsTypeScript type toItemListthat typesitemsasstring[]andonPickas(item: string) => void. Then try passingitems={42}and read the compiler error — that's your contract working. - Deliberately break the rule: inside a child, write
items.push("x")and observe that the parent's data is affected. Then fix it the right way. Say out loud, as in a micro1 screen, why one-way data flow makes the mutation a bug even though nothing threw.
Chapter Summary
- A component is a function; props is its single parameter object, built from the JSX attributes the parent writes.
- Pass string literals in quotes and everything else — numbers, booleans, objects, arrays, functions — in
{}. A bare attribute means={true}. - Read props from the object (
props.x) or, idiomatically, by destructuring in the parameter list. - Props are read-only. Never mutate a prop; it breaks one-way data flow and makes rendering unpredictable. Props come from above; state (Part III) is owned here.
- Give optional props defaults in the destructure — they trigger only on
undefined, unlike||. - Function props (callbacks) let a child signal back up without mutating anything — the teaser for Events.
- Type props with a
type Props = { … }so the compiler enforces the component's contract.
We've passed data in as named attributes — name, age, items. But there's one prop so special React gives it its own syntax: whatever you nest between a component's opening and closing tags. That's the children prop, and it's the key to building components that wrap and compose other components — layouts, cards, modals. Next chapter: The children Prop.
Editorial reflection — props feel almost too simple to dwell on — you're just passing arguments to a function. But the read-only rule sitting on top of that simplicity is one of the most consequential design decisions in modern software. It says: data has an owner, it flows one way, and no one downstream may rewrite history. Everything React gives you — predictable renders, components you can reason about alone, a tree you can trace from the top — is rent paid by that one constraint. Internalise it now, while the examples are small, and the large ones will never surprise you.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.