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. 😊)