34

I have a component that stores a contact object as state - {firstName: "John", lastName: "Doe", phone: "1234567890} I want to create a form to edit this object but if I want the inputs to hold the value of the original contact parameter, I need to make each input a controlled component. However, I don't know how to create a handleChange function that will adjust to each parameter because my state only holds {contact: {...}}. Below is what I currently have -

  getInitialState: function () {
    return ({contact: {}});
  },
  handleChange: function (event) {
    this.setState({contact: event.target.value });
  },
  render: function () {
    return (
        <div>
          <input type="text" onChange={this.handleChange} value={this.state.contact.firstName}/>
          <input type="text" onChange={this.handleChange} value={this.state.contact.lastName}/>
          <input type="text" onChange={this.handleChange} value={this.state.contact.lastName}/>
        </div>
      );
    }

I wish in my handleChange I can do something like

  handleChange: function (event) {
    this.setState({contact.firstName: event.target.value });
  }
Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
akantoword
  • 2,824
  • 8
  • 26
  • 43

9 Answers9

69

There's a "simple" way to do this, and a "smart" way. If you ask me, doing things the smart way is not always the best, because I may be harder to work with later. In this case, both are quite understandable.

Side note: One thing I'd ask you to think about, is do you need to update the contact object, or could you just keep firstName etc. directly on state? Maybe you have a lot of data in the state of the component? If that is the case, it's probably a good idea to separate it into smaller components with narrower responsibilities.

The "simple" way

  changeFirstName: function (event) {
    const contact = this.state.contact;
    contact.firstName = event.target.value;
    this.setState({ contact: contact });
  },
  changeLastName: function (event) {
    const contact = this.state.contact;
    contact.lastName = event.target.value;
    this.setState({ contact: contact });
  },
  changePhone: function (event) {
    const contact = this.state.contact;
    contact.phone = event.target.value;
    this.setState({ contact: contact });
  },
  render: function () {
    return (
      <div>
        <input type="text" onChange={this.changeFirstName.bind(this)} value={this.state.contact.firstName}/>
        <input type="text" onChange={this.changeLastName.bind(this)} value={this.state.contact.lastName}/>
        <input type="text" onChange={this.changePhone.bind(this)} value={this.state.contact.phone}/>
      </div>
    );
  }

The "smart" way

  handleChange: function (propertyName, event) {
    const contact = this.state.contact;
    contact[propertyName] = event.target.value;
    this.setState({ contact: contact });
  },
  render: function () {
    return (
      <div>
        <input type="text" onChange={this.handleChange.bind(this, 'firstName')} value={this.state.contact.firstName}/>
        <input type="text" onChange={this.handleChange.bind(this, 'lastName')} value={this.state.contact.lastName}/>
        <input type="text" onChange={this.handleChange.bind(this, 'phone')} value={this.state.contact.lastName}/>
      </div>
    );
  }



Update: Same examples using ES2015+

This section contains the same examples as shown above, but using features from ES2015+.

To support the following features across browsers you need to transpile your code with Babel using e.g. the presets es2015 and react, and the plugin stage-0.

Below are updated examples, using object destructuring to get the contact from the state, spread operator to create an updated contact object instead of mutating the existing one, creating components as Classes by extending React.Component, and using arrow funtions to create callbacks so we don't have to bind(this).

The "simple" way, ES2015+

class ContactEdit extends React.Component {

  changeFirstName = (event) => {
    const { contact } = this.state;
    const newContact = {
      ...contact,
      firstName: event.target.value
    };
    this.setState({ contact: newContact });
  }
  changeLastName = (event) => {
    const { contact } = this.state;
    const newContact = {
      ...contact,
      lastName: event.target.value
    };
    this.setState({ contact: newContact });
  }
  changePhone = (event) => {
    const { contact } = this.state;
    const newContact = {
      ...contact,
      phone: event.target.value
    };
    this.setState({ contact: newContact });
  }

  render() {
    return (
      <div>
        <input type="text" onChange={this.changeFirstName} value={this.state.contact.firstName}/>
        <input type="text" onChange={this.changeLastName} value={this.state.contact.lastName}/>
        <input type="text" onChange={this.changePhone} value={this.state.contact.phone}/>
      </div>
    );
  }
}

The "smart" way, ES2015+

Note that handleChangeFor is a curried function: Calling it with a propertyName creates a callback function which, when called, updates [propertyName] of the (new) contact object in the state.

class ContactEdit extends React.Component {

  handleChangeFor = (propertyName) => (event) => {
    const { contact } = this.state;
    const newContact = {
      ...contact,
      [propertyName]: event.target.value
    };
    this.setState({ contact: newContact });
  }

  render() {
    return (
      <div>
        <input type="text" onChange={this.handleChangeFor('firstName')} value={this.state.contact.firstName}/>
        <input type="text" onChange={this.handleChangeFor('lastName')} value={this.state.contact.lastName}/>
        <input type="text" onChange={this.handleChangeFor('phone')} value={this.state.contact.lastName}/>
      </div>
    );
  }
}
dance2die
  • 35,807
  • 39
  • 131
  • 194
ArneHugo
  • 6,051
  • 1
  • 26
  • 47
  • i did not think of pulling contact out of the state, changing its prop and then setting it back in. that's genius! my state is actually an array of contact objects {contacts: [{...}, {...}] but I'm assuming I can pull an individual one out as well? at that point tho, would it be worth it to query through this array in order to find that one specific object? – akantoword Mar 13 '16 at 08:16
  • It should never be necessary to loop through the array to find the right contact. Let's assume you have a `ContactList` component showing all contacts and allowing the user to select one to edit. Store selectedContact in the state of `ContactList`. Then, whenever you have a selectedContact, render ``. Propagate changes from ContactEdit back to ContactList if necessary. – ArneHugo Mar 13 '16 at 08:37
  • 7
    What about using e.target.name inside of handleChange (and, of course, set 'name' attribute on inputs) so you can omit second argument – p0rsche Apr 16 '16 at 06:18
  • If that is indeed something one can do, it sounds like a smart solution. – ArneHugo Apr 16 '16 at 15:55
  • 3
    I think you've inverted the order of your parameters in handleChange for the "smart" way. Shouldn't it be: (propertyName, event) instead of (event, propertyName)? – deadcode Nov 01 '16 at 00:40
  • @p0rsche has a good point: `contact[e.target.name] = e.target.value` – Xeoncross Oct 05 '17 at 19:38
  • It'd be nice if there was a way to write the `handleChangeFor` function to change nested properties. Like _contact.phone.home_ and _contact.address.home.streetName_ – PrashanD Feb 17 '18 at 08:24
  • @PrashanD For that use case I believe you should reconsider how you are composing components and managing state. Though it would be possible to adapt the solution in this answer to deeper data structures, I don't think it would be the right solution to your problem. – ArneHugo Feb 19 '18 at 12:32
10

ES6 one liner approach

<input type="text" 
       value={this.state.username}
       onChange={(e) => this.setState({ username: e.target.value })}
       id="username"/>
Stu P.
  • 1,365
  • 1
  • 14
  • 31
  • 1
    Performance warning: If you need to respond to input changes you might want to debounce the function so the above won't work well as it would fire `componentDidUpdate` for every keypress. – Xeoncross Oct 05 '17 at 19:40
  • Article about the performance issue introduced by using an "arrow function": https://flexport.engineering/ending-the-debate-on-inline-functions-in-react-8c03fabd144 – yzorg Jan 29 '18 at 17:20
  • 2/2 GitHub repo: https://github.com/flexport/reflective-bind (See my other comment for author's Medium article/post.) – yzorg Jan 29 '18 at 17:21
3

The neatest approach

Here is an approach that I used in my simple application. This is the recommended approach in React and it is really neat and clean. It is very close to ArneHugo's answer and I thank hm too. The idea is a mix of that and react forms site. We can use name attribute of each form input to get the specific propertyName and update the state based on that. This is my code in ES6 for the above example:

class ContactEdit extends React.Component {

  handleChangeFor = (event) => {
    const name = event.target.name;
    const value = event.target.value;
    const { contact } = this.state;
    const newContact = {
      ...contact,
      [name]: value
    };
    this.setState({ contact: newContact });
  }

  render() {
    return (
      <div>
        <input type="text" name="firstName" onChange={this.handleChangeFor} />
        <input type="text" name="lastName" onChange={this.handleChangeFor}/>
        <input type="text" name="phone" onChange={this.handleChangeFor}/>
      </div>
    );
  }
}

The differences:

  • We don't need to assign state as value attribute. No value is needed
  • The onChange method does not need to have any argument inside the function call as we use name attribute instead
  • We declare name and value of each input in the begening and use them to set the state properly in the code and we use rackets for name as it is an attribute.

We have less code here and vey smart way to get any kind input from the form because the name attribute will have a unique value for each input. See a working example I have in CodPen for my experimental blog application in its early stage.

azad6026
  • 139
  • 1
  • 3
  • 11
2

There are two ways to update the state of a nested object:

  1. Use JSON.parse(JSON.stringify(object)) to create a copy of the object, then update the copy and pass it to setState.
  2. Use the immutability helpers in react-addons, which is the recommended way.

You can see how it works in this JS Fiddle. The code is also below:

var Component = React.createClass({
    getInitialState: function () {
    return ({contact: {firstName: "first", lastName: "last", phone: "1244125"}});
  },
  handleChange: function (key,event) {
    console.log(key,event.target.value);

    //way 1 
    //var updatedContact = JSON.parse(JSON.stringify(this.state.contact));
    //updatedContact[key] = event.target.value;

    //way 2 (Recommended)
    var updatedContact = React.addons.update(this.state.contact, {
        [key] : {$set: event.target.value}
    });
    this.setState({contact: updatedContact});
  },
  render: function () {
    return (
        <div>
          <input type="text" onChange={this.handleChange.bind(this,"firstName")} value={this.state.contact.firstName}/>
          <input type="text" onChange={this.handleChange.bind(this,"lastName")} value={this.state.contact.lastName}/>
          <input type="text" onChange={this.handleChange.bind(this,"phone")} value={this.state.contact.phone}/>
        </div>
      );
    }
});

ReactDOM.render(
  <Component />,
  document.getElementById('container')
);
Mark
  • 3,113
  • 1
  • 18
  • 24
  • thanks for the help, but how did you require addons? i used var update = require('react-addons-update'); as indicated by the page you sent and then change React.adddons.update to just update but then it wouldn't let me change any of the inputs – akantoword Mar 13 '16 at 03:04
  • That's the right way to do it. Is the rest of your code the same as I posted? – Mark Mar 13 '16 at 07:50
  • @Mark Why is using `React.addons.update` in your opinion recommended? I think it would be useful if you could clarify that in your answer. Also, why would you use `JSON.parse(JSON.stringify(this.state.contact))` instead of just `this.state.contact`? – ArneHugo Mar 13 '16 at 08:09
  • If you don't, `this.state` will be modified directly. As per React docs: "NEVER mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable." (https://facebook.github.io/react/docs/component-api.html) – Mark Mar 13 '16 at 08:13
  • IMHO mutating an object that exists in state just before updating state by putting it back in, is fine. Using `JSON.parse(JSON.stringify(...))` all though a project seems like a needlessly cautious way of doing it. – ArneHugo Mar 13 '16 at 08:46
  • If you incorporate this approach into a more complex application, you're going to have a bad time. Why needlessly introduce a race condition? – Mark Mar 13 '16 at 20:12
1

Here is generic one;

handleChange = (input) => (event) => {
    this.setState({
        ...this.state,
        [input]: event.target.value
    });
}

And use like this;

<input handleChange ={this.handleChange("phone")} value={this.state.phone}/>
Mohamed
  • 1,251
  • 3
  • 15
  • 36
0

<input> elements often have a property called name. We can access this name property from the event object that we receive from an event handler:

Write a generalized change handler

constructor () {
    super();
    this.state = {
      name: '',
      age: ''
    };
    this.handleChange = this.handleChange.bind(this);
  }
  handleChange (evt) {      
    this.setState({ [evt.target.name]: evt.target.value });
  }

render () {
    return (
      <form>

        <label>Name</label>
        <input type="text" name="name" onChange={this.handleChange} />

        <label>Age</label>
        <input type="text" name="age" onChange={this.handleChange} />

      </form>
    );
  }

source

Dhruv Raval
  • 1,535
  • 1
  • 8
  • 15
0
updatePrevData=(event)=>{
    let eventName=event.target.name;
   this.setState({
       ...this.state,
       prev_data:{
        ...this.state.prev_data,
        [eventName]:event.target.value
       }

   })

   console.log(this.state)

}
Sagar Mishra
  • 31
  • 1
  • 5
  • 2
    While this code may answer the question, providing additional context regarding why and/or how this code answers the question improves its long-term value. – β.εηοιτ.βε May 31 '20 at 14:14
0

You can do it without duplicate code and easy way

handleChange=(e)=>{
    this.setState({
        [e.target.id]:e.target.value
    })
}


<Form.Control type="text" defaultValue={this.props.allClients.name}  id="clientName"  onChange={this.handleChange}></Form.Control>
<Form.Control type="email" defaultValue={this.props.allClients.email}  id="clientEmail"  onChange={this.handleChange}></Form.Control>
David Buck
  • 3,752
  • 35
  • 31
  • 35
-2
handleChange(event){
    this.setState({[event.target.name]:event.target.value});
    this.setState({[event.target.name]:event.target.value});
  }
Karan Chunara
  • 518
  • 4
  • 15