skip to Main Content

After I alter an array of react elements saved in state, the contents of those elements gets confused. {name} is the same as {name2} becomes untrue. There’s no change to props and no change to state, but clearly the value of the state changes. Check out this gnarly example.

We can get around it by storing data in state instead of React Elements, but that’s not the point.
Why are name and name2 different?

enter image description here

const useState = React.useState;

function Example() {

const SimpleItem = (props) => {
    const [name, setName] = useState(props.name);
    const name2 = props.name;
    return <h4>{name} is the same as {name2}</h4>
}

const DEMO_ARR1 = [
    <SimpleItem name='apple' />,
    <SimpleItem name='orange'/>,
    <SimpleItem name='fish'/>,
    <SimpleItem name='cat'/>,
    <SimpleItem name='peel'/>,
    <SimpleItem name='eyeball'/>
]

const Bad = () => {

    const [eltArr, setEltArr] = useState(DEMO_ARR1);

    return (
        <div>
                    
            {eltArr.map((item, i)=>{
                return <div key={i}>{item}</div>;
            })}

            <button key="button"
                onClick={() => {
                    setEltArr(eltArr.slice(1))
                }}
            >
                Delete index 0
            </button>
        </div>
    );
};

  return Bad();
}

ReactDOM.render(<Example />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

3

Answers


  1. Don’t keep an array of DOM elements, let the map() make the SimpleItem and just keep the names in the array"

    const useState = React.useState;
    
    function Example() {
    
    const SimpleItem = (props) => {
        const [name, setName] = useState(props.name);
        const name2 = props.name;
        return <h4>{name} is the same as {name2}</h4>
    }
    
    const Bad = () => {
    
        const [eltArr, setEltArr] = useState([ 'apple', 'orange', 'fish', 'cat' ]);
    
        return (
            <div>
                        
                {eltArr.map((item, i)=>{
                    return <SimpleItem key={i} name={item} />;
                })}
    
                <button key="button"
                    onClick={() => setEltArr(p => p.slice(1))}
                >
                    Delete index 0
                </button>
            </div>
        );
    };
    
      return Bad();
    }
    
    ReactDOM.render(<Example />, document.getElementById('root'))
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
    <div id="root"></div>
    Login or Signup to reply.
  2. This is the reason to not use array index as key.

    state is stored in a component with specific key.
    This is what happens in the initial render:

    • useState('apple') and prop name = 'apple' in component with key=0
    • useState('orange') and prop name = 'orange' in component with key=1
    • useState('fish') and prop name = 'fish' component with key=2

    So, in the first one we have a state that is equal to ‘apple’.

    when you remove the first element this happens:

    • useState('apple') and prop name = 'orange' in component with key=0
    • useState('orange') and prop name = 'fish' in component with key=1

    The state is already initialized for component with key=0 and it is equal to ‘apple’. You are not unmounting all the elements, so the state is not getting lost.
    component with key=0 holds the state that is equal to 'apple' even if the prop name changes

    You can fix it by giving keys that are not going to change when you add/remove an item

    const DEMO_ARR1 = ["apple", "orange", "fish", "cat", "peel", "eyeball"];
    
    const Bad = () => {
      const [eltArr, setEltArr] = React.useState(DEMO_ARR1);
    
      return (
        <div>
          {eltArr.map((item) => {
            return (
              <div key={item}>   // here we pass the correct key
                <SimpleItem name={item} />
              </div>
            );
          })}
    
          <button
            key="button"
            onClick={() => {
              setEltArr(eltArr.slice(1));
            }}
          >
            Delete index 0
          </button>
        </div>
      );
    };
    

    And what happens now is:

    • useState('apple') and prop name = 'apple' in component with key='apple'
    • useState('orange') and prop name = 'orange' in component with key='orange'

    component with key='orange' holds the useState('orange'), so even if you remove the ‘apple’, key='orange' will still hold the useState('orange')

    Login or Signup to reply.
  3. This is where that innocuous little "key" comes in:

    <SimpleItem key={i} name={item} />
    

    Note that you are giving these elements an internal identifier. When you remove the first element from the array and run this:

    {eltArr.map((item, i)=>{
      return <SimpleItem key={i} name={item} />;
    })}
    

    You are reusing the previous keys, so you are getting back in the first iteration the div with key equal to 1, which is this with name initialized already to "apple":

    <div key="0">
        const [name, setName] = useState(props.name);
        const name2 = props.name;
        return <h4>{name} is the same as {name2}</h4>
    </div>
    

    You are passing in a new props.name, "orange"; however, for this element, name has already been defined as "apple". Once useState has been used to initialize a value in state, the only way to update it is with the setter (setName here). Since you don’t use setName(props.name), name remains set to "apple" and only name2 is set to "orange".

    If you change this line:

    return <SimpleItem key={i} name={item} />;
    

    to this:

    return <SimpleItem key={i+Math.random()*1000} name={item} />;
    

    You’ll have a low chance of a key collision and this will work as expected. If you use some more advanced method to get a unique key every time, then you can guarantee it will always work. One idea might be to add a number to state that keeps track of how many times the button is clicked, increment it each time, and multiple the i by that value.

    The answer from Oktay Yuzcan is much better – just use item for the key!

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search