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.
How it differs from two-way binding
Section titled “How it differs from two-way binding”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)}/>When unidirectional flow helps
Section titled “When unidirectional flow helps”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.
The trade-off
Section titled “The trade-off”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 statefunction 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.
Try it yourself
Section titled “Try it yourself”References