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}>
×
</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 供你玩。
我对 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 上的
您正在修改 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}>
×
</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 供你玩。