DEV Community

Ezell Frazier
Ezell Frazier

Posted on

Why Doesn't React's useState Hook Merge Objects?

I saw a question today about React's useState hook, as there is unexpected behavior compared to this.setState in class components.

Expected Behavior

A user inputs a value and this.setState merges both initial state and the dispatched object from the event handler.

So, if a user types the letter a, state is represented as the merger of,

{ name: 'a' } and { name: '', email: '', phone: '' },

as { name: 'a', email: '', phone: '' }.

export default class ControlledForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      name: '',
      email: '',
      phone: ''
    }
  }
  render() {
    return (
      <form onSubmit={e => e.preventDefault()}>
        <fieldset>
          <label htmlFor="name">Name</label>
          <input
            type="text"
            id="name"
            value={this.state.name}
            onInput={event => this.setState({ name: event.target.value })} 
            // { name: 'a', email: '', phone: '' }
          />
        </fieldset>
      </form>
    )
  }
}

Enter fullscreen mode Exit fullscreen mode

Unexpected Behavior?

A user inputs a value as before. However, setFormValues dispatches an object,

{ name: 'a' }, replacing the initial state object.

function ControlledForm() {
  const [formValues, setFormValues] = useState({
    name: '',
    email: '',
    phone: ''
  })

  return (
    <form onSubmit={e => e.preventDefault()}>
      <fieldset>
        <label htmlFor="name">Name</label>
        <input
          type="text"
          id="name"
          value={formValues.name}
          // { name: 'a' }
          onInput={event => setFormValues({ name: event.target.value })}
        />
      </fieldset>
  )
}
Enter fullscreen mode Exit fullscreen mode

Manually Merging Objects

setState or setFormValues in this context, is a function with a parameter that can be a plain value, an object, or a function with a parameter containing its current state.

We can leverage the function parameter to merge our objects (or arrays).

<input
  type="text"
  id="name"
  value={formValues.name}
  // { name: 'a', email: '', phone: '' }
  onInput={event => setFormValues(values => ({ ...values, name: event.target.value }))}
 />
Enter fullscreen mode Exit fullscreen mode

Why though?

This may feel like a jarring developer experience for those migrating from legacy React code but, this is by design.

If you miss automatic merging, you could write a custom useLegacyState Hook that merges object state updates. However, we recommend to split state into multiple state variables based on which values tend to change together

In other words, it may be more convenient to avoid merging objects altogether. Would one need to merge objects if replacing the previous object achieved the same result?

So, if one has to merge state objects, they have a few options at their disposal.

  • Manual merging of objects within state
  • Individual useState hooks for each object property
  • A reducer (which may be a tad too much for this example)

When working with state, I tend to ask myself, do these variables change together? If yes, I'll go with an object, and know I'll be fine.

Top comments (0)