Reputation: 129
Let me start saying that I know that setState is async, this is not the problem I'm having (I guess)!
I have a table and Im storing each row in an array defined as:
const [rows, setRows] = useState([]);
Im rendering the table as:
rows.map(r => {
if(r.prop == 1) {
return (
<div key={r.position} onClick={e => AddRow(r.position)}>{r.label} </div>
)
}
})
The AddRow function just adds a row at the specified index (r.position) shifting and updating all the rows accordingly. Of course Im updating the rows using setRows([...newRows]);
This works perfectly as long as I'm adding a row at the bottom. But if I add a row at position 2 for example and the rows are 10, rows (the array) contains only 2 rows as it did when the second row was added. Same for row 3, row 4.....it's like the AddRow function has been duplicated for each row and uses the state of "rows" at the moment it was duplicated.
I tried removing the if statement but the problem persists. I also tried using a fake id as key but again, the problem is still there.
I have the feeling Im missing something very simple, any idea?
Thanks a lot
For reference, this is the AddRow function
const AddTile = (atPosition) => {
let newPosition = atPosition + 1;
let newRow = {
position: newPosition,
label: 'something',
prop: 1
}
let tmp = [];
let counter = 0;
let added = false;
for(let t of rows) {
if(newPosition == counter) {
tmp.push(newRow);
counter++;
added = true;
}
t.position = counter;
tmp.push(t);
counter++;
}
if(!added) {
tmp.push(newTile);
}
setRows([...tmp]);
}
Upvotes: 1
Views: 148
Reputation: 202706
It seems the issue is that you are using the element position
properties as the react key, so the keys are not stable, meaning that the data doesn't have React keys that "stick" to them so React knows it's the same object from the previous render.
I suggest decoupling the "index"/"counter"/"position" from the component as a way of identifying it and using as a React key. Use a GUID that stays with the element and use the mapped index
as the position for insertion. You should also use a functional state update in setRows
to update from the previous state versus some state that is closed over in callback scope from the render cycle the callback was created. (Just about any time a state update depends on any previous state you will want to use a functional update.)
import { v4 as uuidV4 } from 'uuid';
...
const addRow = (index) => {
const newRow = {
id: uuidV4(), // <-- new id
label: 'something',
prop: 1
}
setRows(rows => {
const newRows = rows.slice(); // <-- copy previous state
newRows.splice(index, 0, newRow); // <-- insert after index
return newRows; // <-- return new state
});
}
...
rows.map((r, index) => (
<div
key={r.id} // <-- use guid as key
onClick={() => addRow(index)} // <-- use index for insertion
>
{r.label}
</div>
))
Also, it seems you may've been trying to do some filtering based on the prop
value. If so, then do the filter first, then map.
rows
.filter(r => r.prop === 1)
.map((r, index) => (
<div key={r.id} onClick={() => addRow(index)} >
{r.label}
</div>
))
Upvotes: 1