使用 setState 只更新多维数组的一个元素

IT技术 javascript reactjs
2021-04-28 00:19:44

我只想更改多维数组的一个元素

// 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 });

当我通过更改 row 和 col 的值多次运行上述代码时,它开始滞后。我想,这是因为每次都更新所有元素。我只想更新我要更改的元素,而不是所有元素。我该怎么做?

此外,当我在循环语句中运行上面的代码时,它会滞后并且多个元素的变化一起反映。

2个回答

不要更新整个状态,只更新改变的值。

this.setState(prevState => ({
  ...prevState,
  nodes: {
    ...prevState.nodes,
    [row]: {
      ...prevState[row],
      [col]: {
        ...prevState[row][col],
        isVisited: true
      }
    }
  }
}))

你不应该改变状态,要在数组中设置一个项目,你可以使用 map 或者你可以改变一个浅拷贝。由于您有一个多维数组,因此您有多维地图或必须制作多个浅表副本。

下面是一个 map 和浅拷贝的例子:

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]);

这是一个使用优化重新渲染的完整示例(它只渲染改变的东西):

//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>

这是一个使用类的示例。

//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>