Skip to content Skip to sidebar Skip to footer

Unexpected Behavour When Removing An Item From An Array In React

I've searched high and low for an answer to this but I just need some human help. I'm got a simple form where the user can add additional fields. The user can also delete fields th

Solution 1:

It might look like that it deletes the last element because you are using index as identity.

When you use incrementing id instead it makes much more sense

Example

const { Component, useState, useEffect } = React;

class Form_newPriority extends Component {
  constructor(props) {
    super(props);
    this.state = {
      listName: "",
      counter: 1,
      rows: [{ id: 1, fieldValue: "" }],
    };
  }

  addrow = () => {
    const rows = [...this.state.rows];
    const counter = this.state.counter + 1;
    rows.push({
      id: counter,
      fieldValue: "",
    });
    this.setState({ counter, rows });
  };

  removeRow = (idx) => {
    const rows = [...this.state.rows];
    rows.splice(idx, 1);
    this.setState({ rows: rows });
  };

  render() {
    return (
      <div>
        <input
          className="w-full bg-white border border-alice px-4 py-2 mb-1 h-12 rounded-lg"
          type="text"
        ></input>
        <h2>Items:</h2>
        {this.state.rows.map((row, idx) => {
          return (
            <React.Fragment key={row.id}>
              <input
                className="w-half bg-white border border-alice px-4 py-2 mb-4 mr-4  h-12 rounded-lg" 
                type="text"
                placeholder={row.id}
              ></input>
              <button
                className="mb-4 text-red-700 inline-block"
                onClick={() => {
                  this.removeRow(idx);
                }}
              >
                Remove
              </button>
            </React.Fragment>
          );
        })}
        <button
          className="bg-alice hover:bg-gray-400 text-c_base text-sm font-bold py-1 px-2 rounded block mr-4"
          onClick={this.addrow}
        >
          Add row
        </button>
      </div>
    );
  }
}

ReactDOM.render(
    <Form_newPriority />,
    document.getElementById('root')
  );
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<div id="root"></div>

Solution 2:

This is a pretty common issue that is sometimes hard to wrap your head around. It has to do with how react manages dynamic elements like when returned from a map.

The Problem

You're probably already seeing a warning that you should use a key when returning from map. When no key is given, react automatically uses the index of the array element. This is the root of the issue.

React uses this key to keep track of which element is which. So lets say you have a map that returns 3 divs. React will tag each of these divs with its key so it knows what needs updating in the future. The result will be the following:

<div key={0} val='a' />
<div key={1} val='b' />
<div key={2} val='c' />

This works fine for adding and updating. If you update the entry at index 1, react will update the second element by identifying it by key.

The tricky scenario (like you have discovered) is deleting.

Let's say you delete the middle element. The resulting array will now only contain 2 positions. Now your JSX will look like this:

<div key={0} val='a' />
<div key={1} val='c' />

Maybe you can see what's happened already? You deleted the middle element, but to React, you have removed the last element and updated the second. Why? Because the key is based on the index, and the index has changed. React will unmount the div with a key of 2, and keep the div with key of 1. The second div has not been removed, but has been re-rendered with a new prop instead.

The Solution

The root level fix is you need to provide a unique key to each element that will not change over time. The implementation of this may differ based on your use case.

For instance, if your list is guaranteed to have a unique value for each entry, you can use that. This is the simplest solution, but isn't always available.

A solution I have used in the past is to keep a counter in state that increments every time an element is added. I then use the counter to make an id for each element. That way, if an element is deleted, the id will not change.

A Warning

A common mistake people make when faced with this problem is to generate a completely random key for each element using a helper function. This might stop the current issue, but is a very bad practice that might lead to future bugs. If the key changes every render, the element will unmount and remount every render. This won't have any side-effects in simple div example like this, but if your element is another component with state, it will lose its state. If your element is an input, it might lose focus every update. It's tempting, but do not use a randomly generated number for the key.


Post a Comment for "Unexpected Behavour When Removing An Item From An Array In React"