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?
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
Don’t keep an array of DOM elements, let the
map()
make theSimpleItem
and just keep the names in the array"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 propname = 'apple'
in component withkey=0
useState('orange')
and propname = 'orange'
in component withkey=1
useState('fish')
and propname = 'fish'
component withkey=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 propname = 'orange'
in component withkey=0
useState('orange')
and propname = 'fish'
in component withkey=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 propname
changesYou can fix it by giving keys that are not going to change when you add/remove an item
And what happens now is:
useState('apple')
and propname = 'apple'
in component withkey='apple'
useState('orange')
and propname = 'orange'
in component withkey='orange'
component with
key='orange'
holds theuseState('orange')
, so even if you remove the ‘apple’,key='orange'
will still hold theuseState('orange')
This is where that innocuous little "key" comes in:
Note that you are giving these elements an internal identifier. When you remove the first element from the array and run this:
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":
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:
to this:
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!