Two-way data binding in React
Published on in JavaScript and React
Normally you would use one-way data binding in React apps: parent components own data and pass it to children. You can mimic two-way data binding with a custom hook.
Table of contents
One-way data binding
React uses one-way data binding for controlled components: a parent passes to its child the current state value and a function to update the value. For example:
function App() {
const [value, setValue] = useState('John')
const update = (e) => setValue(e.target.value)
const reset = () => setValue('')
return (
<>
<input value={value} onChange={update} />
<div>Hello {value}</div>
<button onClick={reset}>Reset</button>
</>
)
}
This way the input element (the child) never mutates the state.
Instead it calls the update
function (via the onChange
event),
so that the parent can update the state
and pass the new value to the input element.
So far this is very basic stuff.
AngularJS 1 used to have two-way data binding: a parent passes only the state to its child, and the child can directly mutate the state!
- If the parent updates the state, the child will receive the new value.
- If the child updates the state, the parent will receive the new value.
When the parent receives a new value, it can't know where the new value is coming from, and it can't prevent the child from updating the value.
Two-way data binding is nowadays considered a bad practice because it can easily lead to unpredictability (can't know what's changing state) and poor performance. Possibly for other reasons too.
Two-way data binding
On the upside, two-way data binding makes code cleaner as you don't have to manually deal with event listeners and stuff. Just pass a state to a child and you're done.
Sandro Roth's blog post Two-way Data Binding in React shows how you can mimic two-way data binding in React by writing a custom hook. Like this:
function useModel(initialValue = '') {
const [value, setValue] = useState(initialValue)
const update = (e) => setValue(e.currentTarget.value)
return {
model: { value, onChange: update },
setModel: setValue,
}
}
function App() {
const { model, setModel } = useModel('John')
const reset = () => setModel('')
return (
<>
<input {...model} />
<div>Hello {model}</div>
<button onClick={reset}>Reset</button>
</>
)
}
Notice how we are passing (via spreading) only a "model" to the input element. The model contains the current value and a function to update the value from the child.
This is ultimately still one-way data binding –
the parent is still the single source of truth for the state –
but it looks cleaner
and allows reusage of the logic contained in the useModel
hook.
Check out Sandro's article for more details!
(Also thanks to Sandro for our email exchange over this topic. 😊)