B4 · Part 1 · Beginner
Props
Data flows down. A component can read its props and must never write to them.
12On this page
- Props are the function's arguments
- Destructure in the signature
- Props are read-only
- Changes go up as callbacks
- Spreading, and when not to
- Passing elements and functions
- Two props React takes for itself
- children is more flexible than it looks
- Naming that survives contact with other people
- An object prop is a new reference every render
- Checking props without TypeScript
- A prop that changes is not a variable you can watch
Props are the function's arguments
Everything you write as an attribute becomes a key on one object, and that object is the component's only argument.
<Avatar name="Ana" size={48} active />
// arrives as
{ name: "Ana", size: 48, active: true }
A bare attribute with no value is true. Anything that is not a
string needs braces — size="48" passes the string
"48", size={48} passes the number.
Destructure in the signature
function Avatar({ name, size = 40, active = false }) {
return (
<img
src={"/avatars/" + name + ".png"}
width={size}
className={active ? "is-active" : ""}
alt={name}
/>
);
}
Defaults go in the destructuring. There is no separate mechanism for them, and
the old Component.defaultProps is gone in React 19.
Props are read-only
This is not a convention that is nice to follow. It is the rule the whole model rests on.
function Total({ items }) {
items.push({ id: 99 }); // ✗ mutates the parent's array
items = [...items, x]; // ✗ pointless: reassigns a local
const next = [...items, x]; // ✓ a new array of your own
}
A component that mutates a prop is changing data the parent owns, without the parent knowing. React will not re-render the parent, so the screen and the data disagree — and the bug appears somewhere else entirely.
One-way data flow
Data goes down through props; changes go up through callbacks. The component that owns a piece of state is the only one allowed to change it. Everyone below receives the value and a function to ask for a change.
Changes go up as callbacks
function Parent() {
const [count, setCount] = useState(0);
return <Child count={count} onIncrement={() => setCount(count + 1)} />;
}
function Child({ count, onIncrement }) {
return <button onClick={onIncrement}>{count}</button>;
}
Child cannot change count. It can only say "the
button was pressed", and the parent decides what that means. That indirection
is what makes a component reusable in a context its author never saw.
Spreading, and when not to
<Avatar {...user} /> // every key of user becomes a prop
<Avatar {...user} size={64} /> // later wins: size is 64
<Avatar size={64} {...user} /> // user.size wins, if it has one
Spreading is genuinely useful in one place: a wrapper that forwards the props it does not care about to the element underneath.
function Button({ variant = "primary", ...rest }) {
return <button className={"btn btn--" + variant} {...rest} />;
}
<Button variant="ghost" type="submit" disabled onClick={save}>Save</Button>
Everywhere else it hurts, because you can no longer see what a component
receives by reading the call. <Chart {...props} /> tells the
next reader nothing.
Passing elements and functions
Props are not limited to strings and numbers. Anything a JavaScript value can be, a prop can be — including elements and other components.
<Modal title={<strong>Delete?</strong>} onConfirm={remove} />
<List items={rows} renderItem={(row) => <Row key={row.id} {...row} />} />
<Route element={<Dashboard />} />
Two props React takes for itself
<Row key={item.id} ref={rowRef} item={item} />
key and ref look like props and are not. React
strips both before your component is called — key is for
list identity, ref for
reaching a DOM node. Neither appears in
props.
In React 19 ref is finally a normal prop for function components,
so forwardRef is no longer needed for new code. You will still
meet it constantly in existing codebases.
children is more flexible than it looks
<Wrapper>text</Wrapper> // children: "text"
<Wrapper><A /></Wrapper> // children: one element
<Wrapper><A /><B /></Wrapper> // children: an array
<Wrapper>{(x) => <A v={x} />}</Wrapper> // children: a function
The last one is the render prop pattern — the wrapper owns some state and hands it to the caller to render. Hooks replaced most uses of it, but you will still find it in libraries where the wrapper needs to control when and how often the children render.
Naming that survives contact with other people
| Kind | Convention | Example |
|---|---|---|
| Boolean | is/has/can, and positive | isOpen, not isNotClosed |
| Handler prop | on + what happened | onSelect, onDismiss |
| The function itself | handle + what happened | handleSelect |
| Element prop | the slot's name | icon, footer |
Name handlers after what happened, not what should occur.
onDelete ties the child to one outcome;
onConfirm lets the parent decide what confirming means.
An object prop is a new reference every render
<Chart options={{ grid: true }} /> // new object every render
<Chart onZoom={() => zoom(1)} /> // new function every render
Harmless most of the time — creating a small object is cheap. It starts to matter in exactly two places: when the child is memoised, where a new reference defeats the memo entirely; and when the prop ends up in a dependency array, where it makes an effect run on every render. Both are covered later, but the shape is worth recognising now.
Checking props without TypeScript
PropTypes was removed from React in version 19. In a plain
JavaScript project the remaining options are a runtime check at the top of the
component, or JSDoc types that your editor understands:
/** @param {{ items: string[], onPick: (s: string) => void }} props */
function Picker({ items, onPick }) { ... }
The real answer is TypeScript, which is what the intermediate tier covers — props are a function's parameters, so typing them is typing a function.
A prop that changes is not a variable you can watch
A common early instinct is to copy a prop into state so it can be "kept in sync". Almost always wrong — the copy goes stale the moment the prop changes, and now there are two sources of truth for one value.
function Price({ cents }) {
const [display, setDisplay] = useState(cents / 100); // ✗ frozen at first render
const display = cents / 100; // ✓ just compute it
}
If a value can be derived from props, derive it during render. State is for things the component owns, which is the subject of the next chapter.
Interview answer, one sentence
"Props are a component's arguments and they are immutable from the inside. Data flows down as props and changes flow up as callbacks, so the component that owns a value is the only one that can change it — which is what makes a React tree traceable."
Opens in the editor: write it, run it, and check it against real tests.
All 46 for this level →Finished reading?
0 of 57 chapters read in this series