2

I want to change only one element of a multidimensional array

// nodes is a 2-dimensional array
// of 30 rows and 30 columns
  this.state = {nodes};

// updatedNodes is a deep copy of nodes
  updatedNodes[row][col].isVisited = true;
  setState({nodes : updatedNodes });

When I run the above code multiple times by changing the values of row and col, it starts lagging. I guess, its because all the elements are updated every time. I just want to update the element which I am changing, instead of all the elements. How can I do it?

Also, when I run the above code in a looping statement, it lags and changes in multiple elements are reflected together.

desi kart
  • 33
  • 4
  • `setState` is async which means that if you want to make sequential updates, you'll need to wait for the state to change (make use of the `setState` callback) – Titus Oct 26 '19 at 08:24
  • do yourself a favor and use immer: https://immerjs.github.io/immer/docs/introduction – marzelin Oct 26 '19 at 09:34
  • If you experience a lag then you may want to look at [React.memo](https://reactjs.org/docs/react-api.html#reactmemo) and create pure components that don't re render. However; with the code you posted now that may break because you are mutating state in your example. – HMR Oct 26 '19 at 09:35

2 Answers2

2

don't update the whole state and only update the value that changed.

this.setState(prevState => ({
  ...prevState,
  nodes: {
    ...prevState.nodes,
    [row]: {
      ...prevState[row],
      [col]: {
        ...prevState[row][col],
        isVisited: true
      }
    }
  }
}))
albert
  • 464
  • 2
  • 7
  • 2
    How is this upvoted 2 times? OP clearly states nodes is a multi dimensional **array** – HMR Oct 26 '19 at 08:59
  • @HMR I think this is right answer. Please check this [arrays are objects](https://stackoverflow.com/questions/5048371/are-javascript-arrays-primitives-strings-objects#5048482) – Diamond Oct 26 '19 at 09:18
  • Your spread doesn't work at all. You don't copy an array with `{...original}` but with `[...original]` – HMR Oct 26 '19 at 09:20
  • 1
    @HMR I see. You are right. We can't get the length of the updated nodes like `this.state.nodes.length` because **it is not an array-type object** anymore. :) – Diamond Oct 26 '19 at 09:27
  • @Antonio That's because the spread is wrong: `Array.isArray({...[1,2,3]})` is false but `Array.isArray([...[1,2,3]])` is true – HMR Oct 26 '19 at 09:29
  • @HMR I absolutely agree with you. :D – Diamond Oct 26 '19 at 09:30
2

You should not mutate state, to set an item in an array you can use map or you can mutate a shallow copy. Since you have a multi dimensional array you have multi dimensional map or have to make multiple shallow copies.

Here is an example of both map and shallow copy:

const nodes = [
  [{ isVisited: false }, { isVisited: false }],
  [{ isVisited: false }, { isVisited: false }],
];
const row = 0,
  col = 1;
const newNodes = nodes.map((r, rowIndex) =>
  rowIndex !== row
    ? r
    : r.map((c, colIndex) =>
        colIndex !== col ? c : { ...c, isVisited: true }
      )
);
console.log('with map:', newNodes[row][col]);
//mutating a shallow copy
const shallowNew = [...nodes]; //shallow copy of nodes
shallowNew[row] = [...shallowNew[row]]; //shallow copy row
shallowNew[row][col] = {
  ...shallowNew[row][col],
  isVisited: true,
}; //mutate the row copy
console.log('shallow copy:', shallowNew[row][col]);

//to set multiple rows and cols:
const setRowCol = (nodes, [row, col]) =>
  nodes.map((r, rowIndex) =>
    rowIndex !== row
      ? r
      : r.map((c, colIndex) =>
          colIndex !== col ? c : { ...c, isVisited: true }
        )
  );
const setMultiple = (nodes, rowsCols) =>
  rowsCols.reduce(setRowCol, nodes);
const multiple = setMultiple(nodes, [[0, 0], [0, 1]]);
console.log('multiple:', multiple[0]);
console.log('original:', nodes[row][col]);

Here is a fully working example using optimizations for re rendering (it only renders things that changed):

//used so it doesn't log a bunch on first render
let firstRender = true;
//toggles one item or sets value if defined
const toggleItem = (items, [row, col, value]) =>
  items.map((r, rowIndex) =>
    rowIndex !== row
      ? r
      : r.map((c, colIndex) =>
          colIndex !== col
            ? c
            : {
                ...c,
                checked:
                  value === undefined ? !c.checked : value,
              }
        )
  );
//toggles or sets multiple items (used with setting a row)
const setMultiple = (items, rowsCols) =>
  rowsCols.reduce(toggleItem, items);

function App() {
  //setting initial state
  const [state, setState] = React.useState([
    [
      { checked: false },
      { checked: false },
      { checked: false },
    ],
    [
      { checked: false },
      { checked: false },
      { checked: false },
    ],
  ]);
  //when an item changes, useCallback so we don't re create
  //  a new reference every time (optimize for pure component)
  const itemChange = React.useCallback((row, col) => {
    setState(state => toggleItem(state, [row, col]));
  }, []);
  //change a whole row, also optimized with useCallback
  const rowChange = React.useCallback((rowIndex, value) => {
    setState(state =>
      setMultiple(
        state,
        state[rowIndex].map((_, colIndex) => [
          rowIndex,
          colIndex,
          value,
        ])
      )
    );
  }, []);
  //just to prevent a bunch of logs at first render
  Promise.resolve().then(() => (firstRender = false));
  return (
    <table>
      <tbody>
        {state.map((row, rowIndex) => (
          <Row
            key={rowIndex}
            row={row}
            rowIndex={rowIndex}
            itemChange={itemChange}
            rowChange={rowChange}
          />
        ))}
      </tbody>
    </table>
  );
}
//use React.memo to create a pure component
const Row = React.memo(function Row({
  row,
  rowIndex,
  itemChange,
  rowChange,
}) {
  //will only log for changed components
  if (!firstRender)
    console.log('in Row render, index:', rowIndex);
  return (
    <tr>
      <td style={{ backgroundColor: 'gray' }}>
        <input
          type="checkbox"
          onChange={e =>
            rowChange(rowIndex, e.target.checked)
          }
        />
      </td>
      {row.map(({ checked }, index) => (
        <Item
          key={index}
          checked={checked}
          rowIndex={rowIndex}
          colIndex={index}
          // with parent having state you now have to do prop drilling
          itemChange={itemChange}
        />
      ))}
    </tr>
  );
});
//also a pure component using React.memo
const Item = React.memo(function Item({
  checked,
  rowIndex,
  colIndex,
  itemChange,
}) {
  //also only logs for changed components
  if (!firstRender)
    console.log('   in Item render, index:', colIndex);
  return (
    <td>
      <input
        type="checkbox"
        checked={checked}
        onChange={e => itemChange(rowIndex, colIndex)}
      />
    </td>
  );
});
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Here is an example using classes.

//used so it doesn't log a bunch on first render
//  firstRender is not needed to function
let firstRender = true;
//toggles one item or sets value if defined
const toggleItem = (items, [row, col, value]) =>
  items.map((r, rowIndex) =>
    rowIndex !== row
      ? r
      : r.map((c, colIndex) =>
          colIndex !== col
            ? c
            : {
                ...c,
                checked:
                  value === undefined ? !c.checked : value,
              }
        )
  );
//toggles or sets multiple items (used with setting a row)
const setMultiple = (items, rowsCols) =>
  rowsCols.reduce(toggleItem, items);
class App extends React.Component {
  //setting initial state (state cannot be an array)
  state = {
    nodes: [
      [
        { checked: false },
        { checked: false },
        { checked: false },
      ],
      [
        { checked: false },
        { checked: false },
        { checked: false },
      ],
    ],
  };
  constructor(props) {
    super(props);
    //when an item changes
    this.itemChange = function itemChange(row, col) {
      this.setState(state => ({
        nodes: toggleItem(state.nodes, [row, col]),
      }));
    }.bind(this); //bind to set correct this value
    this.rowChange = function rowChange(rowIndex, value) {
      this.setState(state => ({
        nodes: setMultiple(
          state.nodes,
          state.nodes[rowIndex].map((_, colIndex) => [
            rowIndex,
            colIndex,
            value,
          ])
        ),
      }));
    }.bind(this); //bind to set correct this value
  }
  render() {
    //just to prevent a bunch of logs at first render
    Promise.resolve().then(() => (firstRender = false));
    return (
      <table>
        <tbody>
          {this.state.nodes.map((row, rowIndex) => (
            <Row
              key={rowIndex}
              row={row}
              rowIndex={rowIndex}
              itemChange={this.itemChange}
              rowChange={this.rowChange}
            />
          ))}
        </tbody>
      </table>
    );
  }
}
//extends React.PureComponent
class Row extends React.PureComponent {
  render() {
    const {
      row,
      rowIndex,
      itemChange,
      rowChange,
    } = this.props;
    //will only log for changed components
    if (!firstRender)
      console.log('in Row render, index:', rowIndex);
    return (
      <tr>
        <td style={{ backgroundColor: 'gray' }}>
          <input
            type="checkbox"
            onChange={e =>
              rowChange(rowIndex, e.target.checked)
            }
          />
        </td>
        {row.map(({ checked }, index) => (
          <Item
            key={index}
            checked={checked}
            rowIndex={rowIndex}
            colIndex={index}
            // with parent having state you now have to do prop drilling
            itemChange={itemChange}
          />
        ))}
      </tr>
    );
  }
}
//extending React.PureComponent
class Item extends React.PureComponent {
  render() {
    const {
      checked,
      rowIndex,
      colIndex,
      itemChange,
    } = this.props;
    //also only logs for changed components
    if (!firstRender)
      console.log('   in Item render, index:', colIndex);
    return (
      <td>
        <input
          type="checkbox"
          checked={checked}
          onChange={e => itemChange(rowIndex, colIndex)}
        />
      </td>
    );
  }
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
HMR
  • 37,593
  • 24
  • 91
  • 160
  • Can I use callback in class components. My program is written in class component and I cannot deviate to functional component. (PS: I m a beginner) – desi kart Oct 26 '19 at 11:15
  • @desikart Added example using classes, it's not that difficult to convert functional to class components and vice versa. – HMR Oct 26 '19 at 14:09