Mark
Mark

Reputation: 129

In REACTJS, same function has different state depending from where (in the dom) is called

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

Answers (1)

Drew Reese
Drew Reese

Reputation: 202706

Issue

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.

Solution

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

Related Questions