Topic 6.5
One-Way Data Flow: Props Down, Events Up
In one line
In React, data travels one way: parents pass values down as props, and children report events up by calling callback props; siblings talk through their shared parent, and passing props through many layers that don't use them is called prop drilling.
Think of it like this
A river and a phone. Water (data) only flows downhill, from the mountain (the parent) to the villages (children). A village can't push water back up the river. But it can phone the dam upstream and ask it to open the gates. In React, props are the river and callback functions are the phone.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- One-way data flow
- Data moves only from parents to children, through props.
- Read-only props
- A component must never change the props it receives. Only their owner can change the data.
- Callback prop
- A function passed down so a child can report an event, usually named
onSomething. - Props down, events up
- Values travel down as props; news about events travels up by calling callback props.
- Siblings
- Components that share the same parent, like a search box and a results table.
- Prop drilling
- Passing a prop through several components that don't use it, only to reach a deep child.
Step by step
01Build the static tree first
A product page has a SearchBar and a ProductTable. Both depend on the search text and an "in stock only" checkbox. Their closest common parent is FilterableProductTable, so that's where the state goes (Topic 2.7, "Thinking in React").
02Props down
The parent passes the current values to both children. SearchBar shows them in its inputs; ProductTable uses them to decide which rows to show.
03Events up
The parent also passes setters (or small functions) as callbacks. When you type, SearchBar calls onFilterTextChange(e.target.value). It doesn't know that this changes state in the parent; it only reports what happened.
function SearchBar({ filterText, onFilterTextChange }) {
return (
<input
value={filterText}
placeholder="Search…"
onChange={(e) => onFilterTextChange(e.target.value)}
/>
);
}04The round trip
One key press makes a full loop: the child reports, the parent sets state, React renders the parent, and both children get the new props. The table updates even though it never heard from the search box directly.
05When the owner is far away: prop drilling
If App owns user and only Avatar, four levels down, uses it, then Layout, Sidebar and UserMenu must all accept and forward user. That's prop drilling. Keep it while it's short; Phase 9 removes it when it isn't.
Try it yourself
- 1
Add a third sibling
In the product example, add a
<ResultCount products={products} filterText={filterText} inStockOnly={inStockOnly} />that showsN products. Predict: doesSearchBarneed any change? (No. The new sibling reads the same parent state.) - 2
Pass information, not events
Change
SearchBarto callonFilterTextChange(e)with the whole event, and the parent to dosetFilterText(e.target.value). It works, but now the parent knows about input events. Change it back and notice which version makesSearchBareasier to reuse with a different input. - 3
Shorten the drill with children
In the drilling example, make
LayoutandSidebaracceptchildren, and inAppwrite<Layout><Sidebar><UserMenu user={user} /></Sidebar></Layout>. Which components still mentionuser? This composition trick is Topic 9.1.
Code & diagrams
Type `an` or tick the checkbox. `SearchBar` and `ProductTable` never reference each other; the parent owns the two facts and both children read them.
Preview
Console
Nothing logged yet.React freezes the props object in development, so assigning to a prop throws. Nested objects like `item` are not frozen, which is why mutating `item.name` would silently corrupt the parent's state. Report the change instead, and let the owner copy and update.
Preview
Console
Nothing logged yet.Expected console output
props object frozen? true
item inside it frozen? falseThree components accept `user` only to pass it on. Adding a second piece of data (say, `theme`) means editing all of them. Phase 9 fixes this with composition and context.
Preview
Console
Nothing logged yet.Expected console output
Layout only forwards user
Sidebar only forwards user
UserMenu only forwards user
Avatar uses user: AshaBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Assign to a prop
Write a component that changes its own prop, function Badge(props) { props.label = props.label.toUpperCase(); return <span>{props.label}</span>; }, and render <Badge label="new" />.
Break #2
Mutate a nested prop object
In the read-only example, change the button to onClick={() => { item.name = item.name + "!"; }} and click it.
Myth vs fact
Myth
Children can update their parent's state.
Fact
They can only ask, by calling a function the parent gave them. The parent's own code changes its state.
Myth
Two-way binding would be simpler.
Fact
Two-way binding hides who changed what. One-way flow means every change goes through the owner's setter, so bugs are easy to trace. Controlled inputs give you the convenience with the clarity kept.
Myth
Prop drilling is always bad.
Fact
Passing props two or three levels is explicit and easy to follow. It becomes a problem only when many layers exist just to forward data.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
React freezes
element.props(and the element) only in development builds; production skips it for speed, so code that mutates props may "work" in production while being wrong. Treat the dev error as a real bug. - ▸
Passing a state setter directly as a callback (
onFilterTextChange={setFilterText}) is fine and stable: React guarantees setter identity doesn't change between renders. Wrapping it ((t) => setFilterText(t.trim())) creates a new function each render, which matters only formemochildren (Topic 16.4). - ▸
One-way flow is what lets React DevTools answer "why did this render?" and lets the React Compiler reason about your code: a value can only change where its owner sets it. Flux and Redux (Topic 15.4) scaled the same idea to whole apps with a single store and dispatched actions.
- ▸
When a child must notify the parent *and* update its own state, do both in the same event handler. React batches them into one render. Doing the notification in an effect after render causes an extra render pass and a moment of inconsistent UI (Topic 7.6).
Remember this
- 1
One-way data flow (also called unidirectional data flow) means props move only from parent to child. A child receives props and must treat them as read-only: it never changes them. In the development build React even freezes the props object, so
props.label = "x"throws an error. Because data only flows down, you can find where any value comes from by walking up the tree to the component that owns it. - 2
Children still need to say "something happened": a button was clicked, text was typed, an item was deleted. They do it with callback props: functions the parent passes down, named
onSomethingby convention (onSelect,onDelete,onFilterTextChange). The child callsonDelete(item.id); the parent decides what that means and updates its own state. The new value then flows down again. This is "props down, events up". - 3
Callback props describe what happened, not what to do:
onDelete(id), notremoveFromArrayAndSave(id). The child stays reusable (the sameTodoItemcan be used where deletion means something else), and the owner of the state stays the only place that changes it. Pass the information the parent needs as arguments (an id, the new text), not the whole event object, unless the parent truly needs it. - 4
Siblings never talk directly. A search box and a results table are siblings; the box can't reach into the table. Instead their parent owns the shared state (the filter text, Topic 6.3), passes it down to both, and gives the search box a callback to change it. Every sibling conversation goes up to the common parent and back down.
- 5
Notify the parent in the event handler, in the same place you update local state, not later in an effect. Then both updates are batched into one render (Topic 4.5) and nothing renders with half-updated data. Topic 7.6 shows the effect version and why it's slower and buggier.
- 6
Prop drilling is what happens when the state's owner is far above the component that needs it: every component in between must accept the prop and pass it on, even though it doesn't use it. For two or three levels this is fine and very clear. When it spreads across many layers it becomes noisy and fragile. Phase 9 shows the fixes: passing components as
children(composition, Topic 9.1) and context (Topic 9.2).
Explain it without notes
What is one-way data flow, and why does React use it?
How does a child component tell its parent that something happened? Show a small example.
Two sibling components need the same data. How do they communicate?
What is prop drilling, when is it fine, and what are the fixes?
Practice
Write a Rating component that shows 5 star buttons and calls onRate(n) when star n is clicked, and a parent that stores the rating and shows You rated N/5.
A ColorPicker and a Preview box are siblings. Make choosing a colour in the picker change the preview's background.
Trade-offs
- ↔
One-way flow adds wiring (callback props) compared with letting components change shared data directly, but every change has one obvious place where it happens.
- ↔
Prop drilling is explicit but verbose; context removes the wiring but hides where data comes from and re-renders every consumer on change (Topic 9.3).
Done when you can
I treat props as read-only, including nested objects.
I report events up with
onSomethingcallback props that pass information.I connect siblings through their closest common parent.
I notify parents in event handlers, not later.
I can recognise prop drilling and name the fixes coming in Phase 9.