Unidirectional Data Flow
Unidirectional Data Flow
Level 2 — State & Reactivity The strict architectural principle that data in a React application flows in a single direction: top-down from parent to child components via props.
1. Prerequisites
- Props (Properties) — The vehicle that carries data downwards through component trees.
- State — The source of dynamic data that flows top-down.
2. Term Category
Component Pattern (data flow architecture): Unidirectional Data Flow (one-way data binding) is a foundational architectural design pattern in React. Data in a React application strictly moves downwards from parent components to child components via props.
Child components cannot directly mutate parent data or pass props "sideways" to sibling components. To request state updates, child components execute callback functions passed down from parent components, maintaining a clear, single source of truth and predictable state debugging.
3. Explanation
(1) Design Motivation — "Why did we design this?"
In earlier web frameworks (such as AngularJS 1.x), "Two-Way Data Binding" was popular. If a child input changed a variable, it automatically mutated the parent state in-place, and if a parent changed a variable, it mutated the child.
While convenient for simple forms, Two-Way Data Binding created a chaotic web of updates in large applications. If a state variable changed unexpectedly, tracking down which component in a complex hierarchy triggered the mutation became nearly impossible, causing difficult-to-reproduce state synchronization bugs.
React introduced Unidirectional Data Flow to solve this:
- Data flows like a waterfall: strictly top-down from parent to child via props.
- State is owned by a single component at any given time (single source of truth).
- If a child needs to update parent data, it uses Inverse Data Flow: the parent passes down an event callback function prop (e.g.,
onChange), and the child invokes that callback to request a state update in the parent.
┌────────────────────────┐
│ Parent Component │ <── State Source
└────────────────────────┘
│ ▲
(Data Flows DOWN) (Events Flow UP)
│ │
▼ │
┌────────────────────────┐
│ Child Component │ <── Invokes Callback
└────────────────────────┘
(2) Reality Metaphor
Imagine a corporate reporting hierarchy inside a company.
- Two-Way Binding (Chaos): An intern can walk into the CEO's office, rewrite the company budget ledger directly on the CEO's desk without approval, and walk out. When financial audits occur, nobody can trace who altered the balance figures or why.
- Unidirectional Flow (Structured Chain of Command): The CEO (parent component) holds the financial ledger (state). The CEO issues budget summaries down to department managers (props flowing down). If an intern (child component) needs extra funds for software, they submit a purchase request ticket (invoking callback event). The CEO reviews the ticket, updates the ledger, and issues updated budget numbers down the chain (predictable update flow).
(3) React Code Examples
Short Snippet
// Unidirectional flow: Data flows DOWN (props); Events flow UP (callbacks)
function Parent() {
const [text, setText] = useState('Initial');
return (
<div>
{/* Passes data down (value) and callback function up (onChange) */}
<ChildInput value={text} onChange={setText} />
</div>
);
}
function ChildInput({ value, onChange }) {
return <input value={value} onChange={e => onChange(e.target.value)} />;
}
Fuller Example
import React, { useState } from 'react';
// Reader Child Component: receives data read-only via props
function UserDisplay({ user }) {
return (
<div className="user-card">
<h4>User: {user.name}</h4>
<p>Status: {user.status}</p>
</div>
);
}
// Writer Child Component: receives value and fires event callback up
function UserEditor({ currentStatus, onStatusChange }) {
return (
<div className="editor-controls">
<label>Update Status:</label>
<select
value={currentStatus}
onChange={e => onStatusChange(e.target.value)}
>
<option value="Active">Active</option>
<option value="Away">Away</option>
<option value="Offline">Offline</option>
</select>
</div>
);
}
// Single Source of Truth Parent
export default function UnidirectionalDemo() {
const [user, setUser] = useState({ name: 'Alice', status: 'Active' });
const handleStatusChange = (newStatus) => {
// Parent updates state; data flows down automatically on re-render
setUser(prev => ({ ...prev, status: newStatus }));
};
return (
<div className="parent-container">
<h3>Unidirectional Data Flow Demo</h3>
<UserDisplay user={user} />
<UserEditor
currentStatus={user.status}
onStatusChange={handleStatusChange}
/>
</div>
);
}
4. Common Mistakes & Pitfalls
Mistake 1: Attempting to Mutate Parent Props Directly Inside Child Components
The mistake: Writing props.user.status = 'Offline' inside a child component function.
Why it's wrong: React props are read-only. Mutating props directly breaks unidirectional data flow, bypasses parent state tracking, and creates out-of-sync bugs across other components reading the same object.
Incorrect:
function StatusToggle(props) {
const handleToggle = () => {
// ❌ Error: Direct prop mutation breaks unidirectional flow!
props.user.status = 'Offline';
};
return <button onClick={handleToggle}>Set Offline</button>;
}
Fix:
function StatusToggle({ status, onToggleStatus }) {
// ✅ Invoke parent callback to request state change
return <button onClick={() => onToggleStatus('Offline')}>Set Offline</button>;
}
Mistake 2: Copying Props into Local Child State (Anti-Pattern)
The mistake: Receiving a prop and immediately copying it into local state: const [status, setStatus] = useState(props.status).
Why it's wrong: Copying props into local state creates two competing sources of truth. When the parent component updates props.status, the child component's useState ignores the update, causing UI desynchronization.
Incorrect:
function ChildBadge({ status }) {
// ❌ Duplicate state ignores parent status prop updates!
const [localStatus, setLocalStatus] = useState(status);
return <span className="badge">{localStatus}</span>;
}
Fix:
function ChildBadge({ status }) {
// ✅ Read prop directly without local state duplication
return <span className="badge">{status}</span>;
}
Mistake 3: Creating Circular State Update Loops Between Sibling Components
The mistake: Child A updates Parent state in useEffect, which updates Child B props, which triggers Child B useEffect to update Child A.
Why it's wrong: Circular state update chains trigger infinite re-render loops (Maximum update depth exceeded). Always manage state updates through explicit user event handlers or consolidate logic into a single parent component.
Incorrect:
// ❌ Circular state update chains inside useEffect across sibling components
Fix:
// Consolidate state update logic inside explicit parent event handlers
5. Practice Exercises
Exercise 1: IoT Sensor Calibration Control (IoT Telemetry)
Scenario: An industrial IoT monitoring app displays sensor metrics and allows operators to send calibration offset updates using unidirectional flow.
Requirements:
- Create
SensorDisplayreadingoffsetprop. - Create
CalibrationControlwriter firingonCalibratecallback up. - Create
TelemetryNodeparent holdingoffsetstate. - Provide structured implementation with technical explanation.
Answer
Implementation
import React, { useState } from 'react';
function SensorDisplay({ sensorId, reading, offset }) {
const calibratedValue = (reading + offset).toFixed(2);
return (
<div className="display-card">
<h5>Sensor ID: {sensorId}</h5>
<p>Calibrated Reading: {calibratedValue} PSI (Offset: {offset})</p>
</div>
);
}
function CalibrationControl({ currentOffset, onCalibrate }) {
return (
<div className="control-card">
<button onClick={() => onCalibrate(currentOffset + 0.5)}>+0.5 Offset</button>
<button onClick={() => onCalibrate(currentOffset - 0.5)}>-0.5 Offset</button>
<button onClick={() => onCalibrate(0)}>Zero Calibration</button>
</div>
);
}
export function TelemetryNode() {
const [offset, setOffset] = useState(0.0);
return (
<div className="telemetry-node">
<h4>Telemetry Calibration Station</h4>
<SensorDisplay sensorId="S-900" reading={101.3} offset={offset} />
<CalibrationControl currentOffset={offset} onCalibrate={setOffset} />
</div>
);
}
Technical Explanation
- Data Flows Down:
offsetstate travels down toSensorDisplayandCalibrationControlvia props. - Events Flow Up: Button clicks invoke
onCalibratecallbacks, passing numeric deltas up tosetOffset. - Single Source of Truth:
TelemetryNodeholds the single authoritativeoffsetstate. - Predictable Debugging: State transitions can be tracked linearly from parent handlers down.
Exercise 2: Financial Order Ticket Execution Flow (Financial Trading)
Scenario: A trading ticket allows selecting buy/sell sides. The ticket controls side selection and passes data down to execution preview panels.
Requirements:
- Create
SideSelectorwriter component. - Create
ExecutionPreviewreader component. - Create
OrderTicketparent managingside('BUY' | 'SELL') state. - Provide structured implementation with technical explanation.
Answer
Implementation
import React, { useState } from 'react';
function SideSelector({ side, onSelectSide }) {
return (
<div className="side-toggle">
<button
className={side === 'BUY' ? 'active-buy' : ''}
onClick={() => onSelectSide('BUY')}
>
BUY
</button>
<button
className={side === 'SELL' ? 'active-sell' : ''}
onClick={() => onSelectSide('SELL')}
>
SELL
</button>
</div>
);
}
function ExecutionPreview({ side, price, qty }) {
return (
<div className={`preview-card ${side.toLowerCase()}`}>
<h5>Order Summary</h5>
<p>Action: {side} {qty} shares @ ${price}</p>
</div>
);
}
export function OrderTicket() {
const [side, setSide] = useState('BUY');
return (
<div className="ticket-box">
<h4>Trade Ticket</h4>
<SideSelector side={side} onSelectSide={setSide} />
<ExecutionPreview side={side} price={150.25} qty={100} />
</div>
);
}
Technical Explanation
- Top-Down Prop Flow:
sidestate is passed down to both children via props. - Inverse Event Dispatch:
SideSelectortriggersonSelectSidecallbacks on button clicks. - Synchronized Subtrees: Updating
sidein parent re-rendersExecutionPreviewinstantly. - No Direct Sibling Coupling: Sibling components do not interact directly with each other.
Exercise 3: E-Commerce Filter Bar & List Unidirectional Flow (E-Commerce)
Scenario: An e-commerce product page features a category filter bar and product grid using unidirectional flow.
Requirements:
- Create
CategoryFilterwriter component. - Create
ProductGridreader component. - Create
StorePageparent managingselectedCategorystate. - Provide structured implementation with technical explanation.
Answer
Implementation
import React, { useState } from 'react';
function CategoryFilter({ categories, selected, onSelectCategory }) {
return (
<div className="filter-bar">
{categories.map(cat => (
<button
key={cat}
className={selected === cat ? 'selected' : ''}
onClick={() => onSelectCategory(cat)}
>
{cat}
</button>
))}
</div>
);
}
function ProductGrid({ products, category }) {
const filtered = category === 'All'
? products
: products.filter(p => p.category === category);
return (
<div className="product-grid">
{filtered.map(p => (
<div key={p.id} className="item-card">{p.name} ({p.category})</div>
))}
</div>
);
}
export function StorePage({ products }) {
const [category, setCategory] = useState('All');
const categories = ['All', 'Electronics', 'Apparel', 'Books'];
return (
<div className="store-layout">
<CategoryFilter
categories={categories}
selected={category}
onSelectCategory={setCategory}
/>
<ProductGrid products={products} category={category} />
</div>
);
}
Technical Explanation
- Single State Source:
categorystate lives inStorePage. - Unidirectional Props: Data flows down to
CategoryFilterandProductGrid. - Event Notification:
onSelectCategorypasses chosen strings up to the parent setter. - Deterministic Rendering: Eliminates data racing and desynchronized view filters.
6. Related Terms
- Props (Properties) — The vehicle carrying data down through unidirectional component trees.
- State — The source data managed by parent components.
- Lifting State Up — The pattern used to position state in the closest common parent for unidirectional flow.
- Controlled Components — Form inputs governed by unidirectional state and callback props.
7. Key Takeaways
- Unidirectional Data Flow dictates that data strictly flows top-down from parent to child via props.
- Data flows DOWN via props; Events flow UP via callback functions.
- Child components must never mutate incoming props directly.
- Sibling components cannot share data directly; state must be lifted to their common parent.
- Never copy props into local child state (
useState(props.val)), as it creates competing sources of truth.