useState hook React 的意外行为

Unexpected behavior with useState hook React

我对 React 中的 class 组件有一些经验,但我正在尝试更好地学习钩子和功能组件。

我有以下代码:

import React, { useState } from "react";
import { Button } from "reactstrap";
import StyleRow from "./StyleRow";

export default function Controls(props) {
  const [styles, setStyles] = useState([]);

  function removeStyle(index) {
    let newStyles = styles;
    newStyles.splice(index, 1);
    setStyles(newStyles);
  }

  return (
    <div>
      {styles}
      <Button
        color="primary"
        onClick={() => {
          setStyles(styles.concat(
            <div>
              <StyleRow />
              <Button onClick={() => removeStyle(styles.length)}>x</Button>
            </div>
          ));
        }}
      >
        +
      </Button>
    </div>
  );
}

此代码的目标是创建一个组件数组,每个组件旁边都有一个“x”按钮,用于删除该特定组件,底部还有一个“+”按钮,用于添加新组件. StyleRow 组件现在只是 returns 一个段落 JSX 元素。

不寻常的行为是,当我单击一行中的“x”按钮时,它会删除该元素及其后面的所有元素。似乎添加的每个新 StyleRow 组件都在其创建时获取样式状态并对其进行修改,而不是始终修改当前样式状态。这与我对 class 组件的预期不同。

状态冻结让我相信这与关闭有关,我并不完全理解,我很想知道这里是什么触发了它们。如果有人知道如何解决这个问题并且总是修改相同的状态,我将不胜感激。

最后,我认为 SO 上的 post 是相似的,但我相信它解决了一个稍微不同的问题。如果有人可以解释这个答案是如何解决这个问题的,当然可以随时关闭这个问题。提前致谢!

您正在修改 styles 的现有状态,因此您需要先创建数组的深层副本。

您可以编写自己的克隆函数,也可以导入 Lodash cloneDeep 函数。

将以下依赖项添加到您的 package.json 使用:

npm install lodash

此外,您将数组的长度传递给 removeStyle 函数。您应该传递最后一个索引 length - 1.

// ...

import { cloneDeep } from 'lodash';

// ...

  function removeStyle(index) {
    let newStyles = cloneDeep(styles); // Copy styles
    newStyles.splice(index, 1);        // Splice from copy
    setStyles(newStyles);              // Assign copy to styles
  }

// ...

              <Button onClick={() => removeStyle(styles.length - 1)}>x</Button>
// ...

如果您想使用不同的克隆函数或自己编写,这里有一个性能基准:

"What is the most efficient way to deep clone an object in JavaScript?"

我还会将按钮中分配给 onClick 事件处理程序的函数移到 render 函数之外。看起来您正在调用 setStyles,它添加了一个带有 removeStyle 事件的按钮,该事件本身调用 setStyles。将其移出后,您或许可以更好地诊断问题。


更新

我在下面重写了你的组件。尝试使用 map 方法渲染元素。

import React, { useState } from "react";
import { Button } from "reactstrap";

const Controls = (props) => {
  const [styles, setStyles] = useState([]);

  const removeStyle = (index) => {
    const newStyles = [...styles];
    newStyles.splice(index, 1);
    setStyles(newStyles);
  };

  const getChildNodeIndex = (elem) => {
    let position = 0;
    let curr = elem.previousSibling;
    while (curr != null) {
      if (curr.nodeType !== Node.TEXT_NODE) {
        position++;
      }
      curr = curr.previousSibling;
    }
    return position;
  };

  const handleRemove = (e) => {
    //removeStyle(parseInt(e.target.dataset.index, 10));
    removeStyle(getChildNodeIndex(e.target.closest("div")));
  };

  const handleAdd = (e) => setStyles([...styles, styles.length]);

  return (
    <div>
      {styles.map((style, index) => (
        <div key={index}>
          {style}
          <Button data-index={index} onClick={handleRemove}>
            &times;
          </Button>
        </div>
      ))}
      <Button color="primary" onClick={handleAdd}>
        +
      </Button>
    </div>
  );
};

export default Controls;

让我们试着了解这里发生了什么。

<Button
  color="primary"
  onClick={() => {
    setStyles(styles.concat(
      <div>
        <StyleRow />
        <Button onClick={() => removeStyle(styles.length)}>x</Button>
      </div>
    ));
  }}
>
  +
</Button>

第一次渲染: // styles = []

您添加了新样式。 // styles = [<div1>]
来自 div 的移除回调持有对 styles 的引用,其长度现在为 0

您再添加一种样式。 // styles = [<div1>, <div2>]
由于 div1 是之前创建的,现在没有创建,它仍然持有对 styles 的引用,其长度仍然是 0.
div2 现在持有对长度为 1 的 styles 的引用。

现在,您拥有的 removeStyle 回调也是如此。 它是一个闭包,这意味着它持有对其外部函数值的引用,即使在外部函数完成执行之后也是如此。 所以当 removeStyles 被第一个 div1 将执行以下行:

let newStyles = styles; // newStyles []

newStyles.splice(index, 1); // index(length) = 0;
// newStyles remain unchanged

setStyles(newStyles); // styles = [] (new state)

现在假设您已经添加了 5 种样式。这就是每个 div

引用的方式
div1 // styles = [];
div2 // styles = [div1];
div3 // styles = [div1, div2];
div4 // styles = [div1, div2, div3];
div5 // styles = [div1, div2, div3, div4];

那么如果你尝试删除div3会发生什么,下面的removeStyly将执行:

let newStyles = styles; // newStyles = [div1, div2]
newStyles.splice(index, 1); // index(length) = 2;
// newStyles remain unchanged; newStyles = [div1, div2]

setStyles(newStyles); // styles = [div2, div2] (new state)

希望对您有所帮助并解决您的问题。欢迎在评论中提出任何问题。

这里有一个 CodeSandbox 供您尝试并正确理解问题。

我在新答案中添加了最喜欢的方式,因为之前的方式变得太长了。

解释在我之前的回答里

import React, { useState } from "react";
import { Button } from "reactstrap";

export default function Controls(props) {
  const [styles, setStyles] = useState([]);

  function removeStyle(index) {
    let newStyles = [...styles]
    newStyles.splice(index, 1);
    setStyles(newStyles);
  }

  const addStyle = () => {
    const newStyles = [...styles];
    newStyles.push({content: 'ABC'});
    setStyles(newStyles);
  };

  // we are mapping through styles and adding removeStyle newly and rerendering all the divs again every time the state updates with new styles.
  // this always ensures that the removeStyle callback has reference to the latest state at all times.
  return (
    <div>
      {styles.map((style, index) => {
        return (
          <div>
            <p>{style.content} - Index: {index}</p>
            <Button onClick={() => removeStyle(index)}>x</Button>
          </div>
        );
      })}
      <Button color="primary" onClick={addStyle}>+</Button>
    </div>
  );
}

这里有一个 CodeSandbox 供你玩。