Search code examples
reactjsformsdata-binding

Two-way data binding on large forms


(After working through a React.js tutorial, I'm currently coding my first little app to get more practice. So this will be a newbie question and the answer is certainly out there somewhere, but apparently I don't know what to search for.)

Google lists a lot of examples on how to achieve two-way data binding for one input field. But what about large, complex forms, possibly with the option of adding more dynamically?

Let's say my form consists of horizontal lines of input fields. All lines are the same: First name, last name, date of birth and so on. At the bottom of the table, there is a button to insert a new such line. All this data is stored in an array. How do I bind each input field to its respective array element, so that the array gets updated when the user edits a value?

Working example with two lines of two columns each:

import { useState} from 'react';

function App() {
    var [name1, setName1] = useState('Alice');
    var [score1, setScore1] = useState('100');
    var [name2, setName2] = useState('Bob');
    var [score2, setScore2] = useState('200');

    function changeNameHandler1 (e) {
        console.log(e)
        setName1(e.target.value)
    }

    function changeScoreHandler1 (e) {
        setScore1(e.target.value)
    }

    function changeNameHandler2 (e) {
        setName2(e.target.value)
    }

    function changeScoreHandler2 (e) {
        setScore2(e.target.value)
    }

    return (
        <div>
            <table>
                <tbody>
                    <tr>
                        <td><input name="name1" id="id1" type="text" value={name1} onChange={changeNameHandler1} /></td>
                        <td><input name="score1" type="text" value={score1} onChange={changeScoreHandler1} /></td>
                    </tr>
                    <tr>
                        <td><input name="name2" type="text" value={name2} onChange={changeNameHandler2} /></td>
                        <td><input name="score2" type="text" value={score2} onChange={changeScoreHandler2} /></td>
                    </tr>
                </tbody>
            </table>
            {name1} has a score of {score1}<br />
            {name2} has a score of {score2}<br />
        </div>
    );
}

export default App;

How do I scale this up without having to add handler functions for hundreds of fields individually?


Solution

  • You can still store your fields in an object and then just add to the object when you want to add a field. Then map through the keys to display them.

    Simple example:

    import { useState } from 'react'
    
    const App = () => {
      const [fields, setFields ] = useState({
        field_0: ''
      })
    
      const handleChange = (e) => {
        setFields({
          ...fields,
          [e.target.name]: e.target.value
        })
      }
    
      const addField = () => setFields({
        ...fields,
        ['field_' + Object.keys(fields).length]: ''
      })
    
      const removeField = (key) => {
        delete fields[key]
        setFields({...fields})
      }
    
      return (
        <div>
          {Object.keys(fields).map(key => (
            <div>
              <input onChange={handleChange} key={key} name={key} value={fields[key]} />
              <button onClick={() => removeField(key)}>Remove Field</button>
            </div>
          ))}
    
          <button onClick={() => addField()}>Add Field</button>
          <button onClick={() => console.log(fields)}>Log fields</button>
        </div>
      );
    };
    
    export default App;
    

    Here is what I think you are trying to achieve in your question:

    import { useState } from 'react'
    
    const App = () => {
      const [fieldIndex, setFieldIndex] = useState(1)
      const [fields, setFields ] = useState({
        group_0: {
          name: '',
          score: ''
        }
      })
    
      const handleChange = (e, key) => {
        setFields({
          ...fields,
          [key]: {
            ...fields[key],
            [e.target.name]: e.target.value
          }
        })
      }
    
      const addField = () => {
        setFields({
          ...fields,
          ['group_' + fieldIndex]: {
            name: '',
            score: ''
          }
        })
        setFieldIndex(i => i + 1)
      }
    
      const removeField = (key) => {
        delete fields[key]
        setFields({...fields})
      }
    
      return (
        <div>
          {Object.keys(fields).map((key, index) => (
            <div key={key}>
              <div>Group: {index}</div>
              <label>Name:</label>
              <input onChange={(e) => handleChange(e, key)} name='name' value={fields[key].name} />
              <label>Score: </label>
              <input onChange={(e) => handleChange(e, key)} name='score' value={fields[key].score} />
              <button onClick={() => removeField(key)}>Remove Field Group</button>
            </div>
          ))}
    
          <button onClick={() => addField()}>Add Field</button>
          <button onClick={() => console.log(fields)}>Log fields</button>
        </div>
      );
    };
    
    export default App;
    

    You may want to keep the index for naming in which case you can use an array. Then you would just pass the index to do your input changing. Here is an example of using an array:

    import { useState } from 'react'
    
    const App = () => {
      const [fields, setFields ] = useState([
        {
          name: '',
          score: ''
        }
      ])
    
      const handleChange = (e, index) => {
        fields[index][e.target.name] = e.target.value
        setFields([...fields])
      }
    
      const addField = () => {
        setFields([
          ...fields,
            {
              name: '',
              score: ''
            }
        ])
      }
    
      const removeField = (index) => {
        fields.splice(index, 1)
        setFields([...fields])
      }
    
      return (
        <div>
          {fields.map((field, index) => (
            <div key={index}>
              <div>Group: {index}</div>
              <label>Name:</label>
              <input onChange={(e) => handleChange(e, index)} name='name' value={field.name} />
              <label>Score: </label>
              <input onChange={(e) => handleChange(e, index)} name='score' value={field.score} />
              <button onClick={() => removeField(index)}>Remove Field Group</button>
            </div>
          ))}
    
          <button onClick={() => addField()}>Add Field</button>
          <button onClick={() => console.log(fields)}>Log fields</button>
        </div>
      );
    };
    
    export default App;