Skip to content

Unidirectional Data Flow

Unidirectional data flow means state moves in one direction through your component tree. Parent components pass data to children via props. When a child needs to change something, it calls a callback function that the parent supplied — the parent owns the state, the child signals a request. React enforces this pattern at the framework level: props are read-only within the receiving component.

function Parent() {
const [count, setCount] = useState(0)
return (
<Child
value={count}
onIncrement={() => setCount(c => c + 1)}
/>
)
}
function Child({ value, onIncrement }: {
value: number
onIncrement: () => void
}) {
return (
<button onClick={onIncrement}>
{value}
</button>
)
}

The pattern traces back to Flux, Facebook’s application architecture from 2014. React adopted the store → view → action → dispatcher loop, then simplified it into props and callbacks. Every update follows the same path: an event starts in the child, travels up to the state owner, the state updates, and the new value flows back down.

In two-way binding (Angular’s [(ngModel)], Vue’s v-model), a directive binds a variable in both directions simultaneously. The input field reads the value and writes back to it in one declaration. In React, you write both halves explicitly: value={x} for the read direction and onChange={e => setX(e.target.value)} for the write direction. React calls this “controlled components.”

// Two-way binding (Vue)
<input v-model="name" />
// Unidirectional equivalent (React)
<input
value={name}
onChange={e => setName(e.target.value)}
/>

Predictability is the main argument. Because data enters a component through a single channel (props or hooks), tracing where a value came from is mechanical: follow the prop chain up. Debugging tools like React DevTools show the exact prop values at every level. State mutations happen inside setter functions or reducers, not scattered across the tree.

Deep component trees expose the weakness. A button nested five levels down that needs to update state at the top requires passing a callback through every intermediate component — even ones that do nothing with it. This “prop drilling” adds boilerplate and makes refactoring harder. Context APIs and state libraries exist partly to solve this.

// Prop drilling: Button needs to update App state
function App() {
const [theme, setTheme] = useState('light')
return <Page theme={theme} onThemeChange={setTheme} />
}
function Page({ theme, onThemeChange }: Props) {
return <Toolbar theme={theme} onThemeChange={onThemeChange} />
}
function Toolbar({ theme, onThemeChange }: Props) {
return <Button theme={theme} onThemeChange={onThemeChange} />
}
function Button({ theme, onThemeChange }: Props) {
return <button onClick={() => onThemeChange('dark')}>{theme}</button>
}

For applications with straightforward nesting (two or three levels), plain props and callbacks remain the clearest option.


References