Search code examples
arraysreactjsstringkeysetstate

Setting keys for React components mapped from a string array with dynamic length


Problem

This is a question form, a question has many answers. User can add, edit, remove the answers in the form.

The answers is stored in a string array. We will use this array to render the answer input and its corresponding "remove" button.

What I've tried:

  • Set index as key: When remove an element from the array, React failed to render the remaining question (it removes the wrong element), although the values from useState computed correctly.
  • Set value as key: The input element which has the changing text re-rendered, thus it loses focus every time we type in a character.

How can we solve this?

CodeSandbox link: https://codesandbox.io/s/multiplechoicequestionform-2h0vp?file=/src/App.js

import { useState } from "react";

function MultipleChoiceQuestionForm() {
  const [answers, setAnswers] = useState<string[]>([]);

  const addAnswer = () => setAnswers([...answers, ""]); // Add a new empty one at bottom

  const removeAnswerAtIndex = (targetIndex: number) => {
    setAnswers(answers.filter((_, index) => index !== targetIndex));
  };

  const onAnswerChangeAtIndex = (newAnswer: string, targetIndex: number) => {
    const newAnswers = [...answers];
    newAnswers[targetIndex] = newAnswer;
    setAnswers(newAnswers)
  };

  return <form>
    {answers.map((answer, index) =>
      <div
        // I think the problem is the key, how to set this correctly ?
        // Set to index: make removing elements has re-render errors 
        key={index}
        // key={answer}  // Lose focus on each character typed
        style={{ display: "flex" }}
      >
        <input type="text" onChange={(e) => onAnswerChangeAtIndex(e.target.value, index)} />
        <button onClick={(_) => removeAnswerAtIndex(index)}>Remove</button>
      </div>
    )}
    <button onClick={addAnswer}>Add</button>
  </form>
}

Solution

  • I think you forgot to bind your answer values to the input element. Right now all your inputs are uncontrolled which is not the best way to handle dynamic forms where you add or delete items, better make them controlled.

    Just do that:

    <input
      type="text"
      value={answer}
      onChange={(e) => onAnswerChangeAtIndex(e.target.value, index)}
    />
    

    Working Codesandbox example

    Other good practice here might be using some other structure for answers, for example, instead of string create an object with id (you can generate them by yourself, as an easiest way just use Math.random()) and value property for each answer, that way you could use that id as real key.