Handling an Unknown Number of Inputs in React

Viewed 122

I am creating a react element which allows users to send emails to friends/family. The element defaults to 3 input fields however, the user can click a button to add additional input fields.

My idea to build this component is to have a button which increments a counter and then to use that counter value to run a loop and add items to an array

<button onClick={handleAddEmail}> Add email </button>

function handleAddEmail() {
  setNumberOfEmails(numberOfEmails + 1);
    }

Now for this to work I need an NumberOfEmails hook. This hook will be initialized with the first three input fields.

 const [numberOfEmails, setNumberOfEmails] = useState(3);

Additionally there is a inviteEmails hook to handle the state of each input.

const [inviteEmails, setInviteEmails] = useState([])

All of these inputs should be rendered in a function:

function renderEmailFields() {
    const emailContainer = [
        <input 
          value={inviteEmails[0]}
          onChange={(e) => handleEmailChange(0, e)}
          placeholder="Enter email here..."
        />,
       <input 
          value={inviteEmails[1]}
          onChange={(e) => handleEmailChange(1, e)}
          placeholder="Enter email here..."
        />,
       <input 
          value={inviteEmails[2]}
          onChange={(e) => handleEmailChange(2, e)}
          placeholder="Enter email here..."
        />];
        for (let i = 3; i < numberOfEmails; i++) {
            emailContainer.push(
                <input
                    value={inviteEmails[i]}
                    onChange={(e) => handleEmailChange(i, e)}
                    placeholder="New email..."
                />
            );
        }
        return emailContainer;


The code I have above is close to working however adding a new input field causes the previous inputs to lock and become uneditable.

Any advice would be appreciated.

1 Answers

You don't really need a second state atom for the number of inputs if you ask me.

See live CodeSandbox here.

  • The useCallback and dataset magic make it so you don't need a separate onClick handler function for each input.
  • .filter(Boolean) removes all falsy values; empty strings are falsy.
  • For the heck of it, this also lets you remove items from the array.
function App() {
  const [inviteEmails, setInviteEmails] = React.useState(["", "", ""]);
  const handleEmailChange = React.useCallback((event) => {
    const index = parseInt(event.target.dataset.index, 10);
    setInviteEmails((inviteEmails) => {
      const newInviteEmails = [...inviteEmails];
      newInviteEmails[index] = event.target.value;
      return newInviteEmails;
    });
  }, []);
  const removeEmail = React.useCallback((event) => {
    const index = parseInt(event.target.dataset.index, 10);
    setInviteEmails((inviteEmails) => {
      const newInviteEmails = [...inviteEmails];
      newInviteEmails.splice(index, 1);
      return newInviteEmails;
    });
  }, []);
  const addEmail = React.useCallback(
    () => setInviteEmails((inviteEmails) => [...inviteEmails, ""]),
    []
  );

  return (
    <div>
      {inviteEmails.map((email, index) => (
        <div key={index}>
          <input
            value={email}
            data-index={index}
            onChange={handleEmailChange}
            placeholder="Enter email here..."
          />
          <button onClick={removeEmail} data-index={index}>
            &times;
          </button>
        </div>
      ))}
      <button onClick={addEmail}>Add email</button>
      <pre>{JSON.stringify(inviteEmails.filter(Boolean), null, 2)}</pre>
    </div>
  );
}
Related