A bug appears. You trace it. The state changed somewhere. But where? In the component? In the template? In the binding? In some lifecycle hook you forgot existed?
This is the cost of two-way data flow. And it's why the best frontend architectures abandoned it.
One Direction, No Surprises
The dispatch-reducer pattern makes data flow obvious. An action goes in. A new state comes out. Nothing else happens.
// Reducer function
const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
default:
return state;
}
};
// Dispatch an action
dispatch({ type: 'INCREMENT' });You dispatched INCREMENT. The state went from 0 to 1. There's no mystery about what happened, why, or where. Every state transition is explicit, traceable, and testable in isolation.
React's own state model works the same way. You call a setter. React re-renders. The render doesn't change the state — it just reflects it.
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};Data flows down. Events flow up. At any point, you can look at the state and know exactly what the UI should be. No hidden feedback loops.
Two-Way Binding: Convenient Until It Isn't
Two-way data binding feels productive at first. You type in an input, the model updates automatically. The model changes, the input updates. Magic.
// Angular example with two-way data binding
<input [(ngModel)]="username">The problem is that magic doesn't debug well. When username has the wrong value, did the user type something? Did a service update it? Did a parent component override it? Did a lifecycle hook reset it? The binding works in both directions, so the source of truth is everywhere and nowhere.
I've seen teams spend hours tracing state bugs in Angular apps that would have been immediately obvious in a unidirectional architecture. The two-way convenience saves you minutes of typing and costs you hours of debugging.
Testing gets harder too. When the view and the model are tightly coupled, you can't test one without simulating the other. Your unit tests stop being units.
The Tradeoff Is Clear
Two-way binding is less code to write. One-way flow is less code to debug. In any non-trivial application, you spend far more time debugging than writing.
The frameworks that won — React, the Elm architecture, Redux — all bet on one-way data flow. Not because it's easier to learn, but because it's easier to trust.
If you can't trace how your state changed by reading the code, your architecture is lying to you.
