Search code examples
reactjstypescriptfluentui-reactdetailslist

React custom editable grid is preserving values from deleted rows


I am using fluent-ui's DetailsList to render a table. I am trying to convert this into a simple editable grid. The 1st column is a readonly string, the 2nd column is an editable TextField and the 3rd column is the delete button. I have provided ItemColumn rendering function so that each column can be rendered as desired. See the following demo:

https://codesandbox.io/p/sandbox/detailslist-delete-broken-gfnrgn?file=%2Fsrc%2Findex.tsx%3A74%2C43

I have provided 2 rows, with the first row having the default value in the textfield of "Blue", and the second row having blank.

The problem: when I click on the delete button in the first row, the color is preserved at that index. So now it looks like Beagal's favorite color is "Blue", when it was in fact John's. This only seems to be a problem with Input columns, and not readonly columns like "Name" in this example, which is why "Beagal" correctly shifts up upon delete of the row above it. This is extra confusing because when I log the listItems state variable, it looks correct, so it's like the grid isn't properly re-rendering the TextField or something.

How to I ensure that when I delete a row, the TextField doesn't show the data from the row below it?


Solution

  • The <TextField> is using a defaultValue. This means the component is "uncontrolled" in the sense it manages its state internally. Seemingly because of a slightly unexpected quirk of Fluent's behaviour, when the defaultValue changes it is ignored and it retains the internal copy it already has.

    You can demonstrate this by setting a key that forces the text field to remount completely when the row deletion causes that item to be in a new position:

    <TextField key={item.name + index} defaultValue={item.favColor}></TextField>
    

    This causes all the TextFields to essentially be removed and readded -- circumventing the problem since the TextField will be forced to grab the new initial value.

    But I suspect you actually wanted to be editing the dataset itself with the new value. I.e. using TextField as a controlled component via the value prop. The onChange prop here then actually updates the favColor field for the data for the row in question (with the new value, each time a keystroke happens) by deep updating listItems.

            case "favColorColumn": {
              return (
                <TextField
                  onChange={(e, newVal) => {
                    setListItems((prevListItems) =>
                      Object.assign([], prevListItems, {
                        [index]: { ...prevListItems[index], favColor: newVal },
                      })
                    );
                  }}
                  value={item.favColor}
                ></TextField>
              );
            }