'A component is changing a controlled input to be uncontrolled' and partial state update

Viewed 44

I have a parent component that is interested in gathering a list of contacts (name+email):

import { useState } from 'react'
import AddContactFn from './components/AddContactFn'

function App() {
    const [contacts, setContacts] = useState([])

    const addNewContact = (newContact) => {
        setContacts([...contacts, newContact])
        
    }

    return (
        <div>
            <AddContactFn newContact={addNewContact} />
        </div>
    )
}

export default App

through a child component, which renders a form, with two inputs fields and an 'Add' button:


import React from 'react'
import { useState } from 'react'

export default function AddContactFn({ newContact }) {
    const [contact, setContact] = useState({name: ' ', email: ' '})

    const addContact = (e) => {
        e.preventDefault()
        if (contact.name === ' ' || contact.email === ' ') {
            return
        }
        newContact(contact)
    }

    return (
        <div>
            <h2>Add Contact</h2>
            <form onSubmit={addContact}>
                <label>Name</label>
                <input
                    type='text'
                    name='name'
                    value={contact.name}
                    placeholder='Name'
                    onChange={e => setContact({ name: e.target.value })}
                />
                <label>Email</label>
                <input
                    type='text'
                    name='email'
                    value={contact.email}
                    placeholder='Email'
                    onChange={e => setContact({ email: e.target.value })}
                />
                <button type='submit'>Add</button>
            </form>
        </div>
    )
}

I am facing two issues:

  1. Warning: A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component.
  2. with both input fields containing text, every time the Add button is pressed, only the last edited value is returned to the parent (ie. either 'name' or 'email' is returned, not both)
3 Answers

Lets fix one at a time

  1. The warning:

Try adding a check for the input value like value={contact.name || ''} The OR || operator will check if contact.name is valid, if so use it otherwise ''

This is a side effect linked to the second issue

  1. The state:

Your onChange handler needs to add the previous object properties, currently they are not, so try onChange={e => setContact(prevState, ({...prevState, name: e.target.value }))}

By doing onChange={e => setContact({ name: e.target.value })} you are losing all the properties of the state object, and setting the state to only have a name or email property, that is why you get the warning since the name or email are lost.

Since you have two input fields (name & email) you will have to do the same on both:

<label>Name</label>
<input
   type='text'
   name='name'
   value={contact.name || ''}
   placeholder='Name'
   onChange={e => setContact(prevState => ({...prevState, name: e.target.value}))}
 />
 <label>Email</label>
 <input
   type='text'
   name='email'
   value={contact.email || ''}
   placeholder='Email'
   onChange={e => setContact(prevState => ({...prevState, email: e.target.value}))}
/>

When you use setContact({ name: e.target.value }) (or for the email) you are also setting the other field as undefined. The way I would recommend you did this is:

import React from 'react'
import { useState } from 'react'

export default function AddContactFn({ newContact }) {
    const [name, setName] = useState(' ');
    const [email, setEmail] = useState(' '); 

    const addContact = (e) => {
        e.preventDefault()
        if (name === ' ' || email === ' ') {
            return
        }
        newContact({ name, email })
    }

    return (
        <div>
            <h2>Add Contact</h2>
            <form onSubmit={addContact}>
                <label>Name</label>
                <input
                    type='text'
                    name='name'
                    value={name}
                    placeholder='Name'
                    onChange={e => setName(e.target.value)}
                />
                <label>Email</label>
                <input
                    type='text'
                    name='email'
                    value={email}
                    placeholder='Email'
                    onChange={e => setEmail(e.target.value)}
                />
                <button type='submit'>Add</button>
            </form>
        </div>
    )
}

If you want to keep the state as it currently is then you can just do e.g. setContact({ ...contact, name: e.target.value })

You are using Controlled Input where you bind the input tag value to one of contact state property and the onChange event to an arrow function that will update the contact state.

Initially, the contact state has two properties, which are name, and email.

const [contact, setContact] = useState({name: ' ', email: ' '})
...
return (
  <input
    value={contact.name}
    onChange={e => setContact({ name: e.target.value })}
  />
  <input
    value={contact.email}
    onChange={e => setContact({ email: e.target.value })}
  />
)

When the onChange event occurs, it will update the contact state with a new object that only has one property. Here, React will raise a warning as one of the input values detects the change and found that the binding reference is missing/undefined. At that moment, your input has become an Uncontrolled Input. That's why you got this error:

Warning: A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component.

To fix it, you can clone the contact by using a shallow copy and update your setter like this:

onChange={e => setContact({ ...contact, name: e.target.value })}

onChange={e => setContact({ ...contact, email: e.target.value })}

So, whenever onChange event occurs, the updated contact state will always have two properties both name, and email. Of course, you can use the other technique, as long as the updated value always has consistent keys and the binding reference remain.

Here is an example:

Edit ecstatic-mclean-9vmjkj

Related