Select fields not being populated by state

Viewed 20

I have created a page with a multiple state controlled input fields but am having difficulty ensuring that they retain the correct text values on the front end after deleting a row of elements.

Here is an image of the component:

enter image description here

Here is the component which displays the inputs:

<InputContainer>
              <PrimaryLabel>Criteria</PrimaryLabel>
              {currentRuleCriteria.map((criteria, index) => (
                  <TripleInputContainer>
                      <StyledTriplePrimarySelect name="criteria-column" id="criteria-column-select" value={currentRuleCriteria[index].column} onChange={(e) => handleCriteriaColumnChange(index, e.target.value)}>
                          {columns.map((option, index) => (
                              <option key={index} value={option.value}>
                                  {option.text}
                              </option>
                          ))}
                      </StyledTriplePrimarySelect>
                      <StyledTriplePrimarySelect name="criteria-operator" id="criteria-operator-select" value={currentRuleCriteria[index].operator} onChange={(e) => handleCriteriaOperatorChange(index, e.target.value)}>
                          {criteriaOperators.map((option, index) => (
                              <option key={index} value={option.value}>
                                  {option.text}
                              </option>
                          ))}
                      </StyledTriplePrimarySelect>
                      <InputTypeSelectorsContainer>
                          <StyledColumnIcon active={criteria.valueType === "column"} onClick={() => handleCriteriaValueTypeChange(index, "column")} />
                           <StyledTextIcon active={criteria.valueType === "text"} onClick={() => handleCriteriaValueTypeChange(index, "text")} />
                      </InputTypeSelectorsContainer>
                      { criteria.valueType === "column" ? (
                          <StyledPrimarySelect name="criteria-value" id="criteria-value-select" value={currentRuleCriteria[index].value} onChange={(e) => handleCriteriaValueChange(index, e.target.value)}>
                              {columns.map((option, index) => (
                                 <option key={index} value={option.value}>
                                      {option.text}
                                 </option>
                              ))}
                          </StyledPrimarySelect>
                      ) : (
                          <StyledPrimaryInput type="text" placeholder="" value={currentRuleCriteria[index].value} onChange={(e) => handleCriteriaValueChange(index, e.target.value)} />
                      )}
                      <RemoveItemContainer onClick={() => handleRemoveCriteria(index)}>
                          <StyledRemoveItemIcon active={currentRuleCriteria.length > 1} />
                      </RemoveItemContainer>
                   </TripleInputContainer>
              ))}
              <AdditionalEntryContainer>
                  <AdditionalEntryButton onClick={() => handleAddCriteria()}>+ Add another</AdditionalEntryButton>
              </AdditionalEntryContainer>
</InputContainer>

Here is the state which is mapped over to produce the input elements:

    const [currentRuleCriteria, setCurrentRuleCriteria] = useState([{
        column: "title",
        operator: "contains",
        value: "",
        valueType: "text"
    }]);

Here are the handler functions for interacting with state:

    const handleCriteriaColumnChange = (index, value) => {
        const newCriteria = [...currentRuleCriteria];
        newCriteria[index].column = value;
        setCurrentRuleCriteria(newCriteria);
    };

    const handleCriteriaOperatorChange = (index, value) => {
        const newCriteria = [...currentRuleCriteria];
        newCriteria[index].operator = value;
        setCurrentRuleCriteria(newCriteria);
    };

    const handleCriteriaValueChange = (index, value) => {
        setCriteriaValue(value);
        const newCriteria = [...currentRuleCriteria];
        newCriteria[index].value = value;
        setCurrentRuleCriteria(newCriteria);
    };

    const handleCriteriaValueTypeChange = (index, value) => {
        const newCriteria = [...currentRuleCriteria];
        newCriteria[index].valueType = value;
        setCurrentRuleCriteria(newCriteria);
        setCriteriaValueType(value);
    };

    const handleAddCriteria = () => {
        setCurrentRuleCriteria([...currentRuleCriteria, {
            column: criteriaColumn,
            operator: criteriaOperator,
            value: criteriaValue,
            valueType: criteriaValueType
        }]);
        setCriteriaColumn("title");
        setCriteriaOperator("contains");
        setCriteriaValue("");
        setCriteriaValueType("text");
    };

    const handleRemoveCriteria = (index) => {
        const newCriteria = [...currentRuleCriteria];
        newCriteria.splice(index, 1);
        setCurrentRuleCriteria(newCriteria);
    };

The Problem:

I can see that when I add a new criteria a new set of input fields are added. Similarly, when I edit the values the state is always maintained correctly. The issue I am having is when deleting a pre-existing row of elements.

If I have two criteria in place, as in the image above, and I select the first one to be deleted - the state updates correctly, however the incorrect input fields seem to be deleted on the frontend and so it no longer aligns with the state values. In this example the following would then be visible:

enter image description here

I don't understand this as the input fields should be being generated by mapping over the currentRuleCriteria state value.

I have tried many things to try and rectify this, but can't seem to figure it out. I previously did not have the value specified on the individual select elements and was in the same situation as I am currently. Adding in a value field and referencing the state did not change anything and so I believe the select fields are not being populated by state values.

Any thoughts? Thank you!

1 Answers

You should set the key on TripleInputContainer:

<TripleInputContainer key={criteria.id}>

React docs says "Keys should be stable, predictable, and unique". But in your case there may be complete duplicates of the criteria. So it makes sense to use Math.random():

const [currentRuleCriteria, setCurrentRuleCriteria] = useState([
  {
    id: Math.random(),
    column: "title",
    operator: "contains",
    value: "",
    valueType: "text",
  },
]);

setCurrentRuleCriteria([
  ...currentRuleCriteria,
  {
    id: Math.random(),
    column: criteriaColumn,
    operator: criteriaOperator,
    value: criteriaValue,
    valueType: criteriaValueType,
  },
]);

It's even better to use nanoid and not worry about collisions.

Related