避免双重使用效果中的陈旧状态?
Avoiding stale state in double useEffect?
我在显示单个数据项的组件 (InternalComponent) 中有两个 useEffect 挂钩。一个 useEffect 将计数作为状态进行跟踪,当用户增加计数时 POSTing 到数据库。当 InternalComponent 跟踪的项目发生变化(由于外部操作)时,第二个 useEffect 会重置计数。
问题是:更新数据库的useEffect会在项目改变时触发;它会看到新的项目值,但会错误地向数据库发送 previous 项目的计数。发生这种情况是因为两个 useEffects“同时”触发,指示计数不应 POSTed 的状态直到 POST useEffect 为 运行.
const InternalComponent = ({ item }) => {
const [count, setCount] = useState(item.count);
const [countChanged, setCountChanged] = useState(false);
useEffect(() => {
console.log(
`Item is now ${item.id}; setting count from item and marking unchanged.`
);
setCount(item.count);
setCountChanged(false);
}, [item]);
useEffect(() => {
if (countChanged) {
console.log(
`Count changed for item ${item.id}, POSTing (id=${item.id}, count=${count}) to database.`
);
} else {
console.log(
`Count hasn't changed yet, so don't update the DB (id=${item.id}, count=${count})`
);
}
}, [item, count, countChanged]);
const handleButtonClick = () => {
setCount(count + 1);
setCountChanged(true);
};
return (
<div>
I'm showing item {item.id}, which has count {count}.<br />
<button onClick={handleButtonClick}>Increment item count</button>
</div>
);
};
Code Sandbox 上的最小工作示例:https://codesandbox.io/s/post-with-stale-data-vfzh4j?file=/src/App.js
带注释的输出:
1 (After button click) Count changed for item 1, POSTing (id=1, count=6) to database.
2 (After item changed) Item is now 2; setting count from item and marking unchanged.
3 Count changed for item 2, POSTing (id=2, count=6) to database.
4 (useEffect runs twice) Count hasn't changed yet, so don't update the DB (id=2, count=50)
第 3 行是不需要的行为:数据库将收到一个 POST,其中包含错误的项目 ID,理想情况下根本不应该发送。
这感觉像是一个 simple/common 问题:我应该使用什么设计来防止 POST useEffect 在陈旧状态下触发?所有我能轻易想到的解决方案在我看来都是荒谬的。 (例如,为每个项目创建一个 InternalComponent 并只显示其中一个,将所有 useEffects 组合成一个巨大的 useEffect 来跟踪组件中的每个状态等)我确定我忽略了一些显而易见的事情:有什么想法吗?谢谢!
好吧,你不是用它自己的 setter 函数更新 items
,而是将它置于 count
状态,这使得逻辑无处不在,非常困难阅读,老实说,我在这里提出一个可能解决您问题的不同解决方案:
const InternalComponent = ({ items, setItems, index }) => {
useEffect(() => {
console.log("item count changed: ", items[index].count);
}, [items, index]);
const handleButtonClick = () => {
setItems(
items.map((item) =>
item.id - 1 === index ? { ...item, count: item.count + 1 } : { ...item }
)
);
};
return (
<div>
I'm showing item {items[index].id}, which has count {items[index].count}.
<br />
<button onClick={handleButtonClick}>Increment item count</button>
</div>
);
};
此方法保持更新后的状态,如果您想在单击 next item
后默认返回其原始 count
值,应该很容易做到。
让我知道这种方法是否解决了问题
问题是由于使用 item
作为两个 useEffect
挂钩的依赖项引起的。当 item
属性更新时,第二个 useEffect
钩子被旧的 count
状态触发,然后在第一个 useEffect
钩子更新 count
之后再次触发第二次状态。
您基本上只想在项目道具更新时“重置”InternalComponent
状态。只需使用项目的 id
作为 InternalComponent
.
上的 React 键
示例:
<InternalComponent key={item.id} item={item} />
然后密钥更改 React 丢弃(卸载)先前的实例并安装一个新实例,新实例已按预期初始化。
因为 InternalComponent
是 remounted/reset,第二个 useEffect
钩子现在完全无关,可以删除。状态值将采用新的 item
属性值。
const InternalComponent = ({ item }) => {
const [count, setCount] = useState(item.count);
const [countChanged, setCountChanged] = useState(false);
useEffect(() => {
if (countChanged) {
console.log(
`Count changed for item ${item.id}, POSTing (id=${item.id}, count=${count}) to database.`
);
} else {
console.log(
`Count hasn't changed yet, so don't update the DB (id=${item.id}, count=${count})`
);
}
}, [item, count, countChanged]);
const handleButtonClick = () => {
setCount(count + 1);
setCountChanged(true);
};
return (
<div>
I'm showing item {item.id}, which has count {count}.<br />
<button onClick={handleButtonClick}>Increment item count</button>
</div>
);
};
给 <InternalComponent />
一个唯一的 key
,所以它卸载并再次重新安装绝对是一个简单组件的方法,如@drew-reese[=15= 所述]
此外,如果我可以逃脱的话,我也支持对数据库或 API 进行 post 访问是一种有意识的行为。使它们成为 useCallback
中的显式操作(将计数、AND、post 添加到数据库)对于简单的更新场景来说是一个不错的选择。
至于为什么当你在示例代码中切换项目时,你得到不需要的 'POSTing' 到数据库,这只是因为你自己的错误,你没有将 countChange
重置为 false在你的代码中一旦你有 'POST' 它。
//...
useEffect(() => {
if (countChanged) {
//ADD THIS
setCountChanged(false);
console.log(
`Count changed for item ${item.id}, POSTing (id=${item.id}, count=${count}) to database.`
);
} else {
console.log(
`Count hasn't changed yet, so don't update the DB (id=${item.id}, count=${count})`
);
}
}, [item, count, countChanged]);
//...
我在显示单个数据项的组件 (InternalComponent) 中有两个 useEffect 挂钩。一个 useEffect 将计数作为状态进行跟踪,当用户增加计数时 POSTing 到数据库。当 InternalComponent 跟踪的项目发生变化(由于外部操作)时,第二个 useEffect 会重置计数。
问题是:更新数据库的useEffect会在项目改变时触发;它会看到新的项目值,但会错误地向数据库发送 previous 项目的计数。发生这种情况是因为两个 useEffects“同时”触发,指示计数不应 POSTed 的状态直到 POST useEffect 为 运行.
const InternalComponent = ({ item }) => {
const [count, setCount] = useState(item.count);
const [countChanged, setCountChanged] = useState(false);
useEffect(() => {
console.log(
`Item is now ${item.id}; setting count from item and marking unchanged.`
);
setCount(item.count);
setCountChanged(false);
}, [item]);
useEffect(() => {
if (countChanged) {
console.log(
`Count changed for item ${item.id}, POSTing (id=${item.id}, count=${count}) to database.`
);
} else {
console.log(
`Count hasn't changed yet, so don't update the DB (id=${item.id}, count=${count})`
);
}
}, [item, count, countChanged]);
const handleButtonClick = () => {
setCount(count + 1);
setCountChanged(true);
};
return (
<div>
I'm showing item {item.id}, which has count {count}.<br />
<button onClick={handleButtonClick}>Increment item count</button>
</div>
);
};
Code Sandbox 上的最小工作示例:https://codesandbox.io/s/post-with-stale-data-vfzh4j?file=/src/App.js
带注释的输出:
1 (After button click) Count changed for item 1, POSTing (id=1, count=6) to database.
2 (After item changed) Item is now 2; setting count from item and marking unchanged.
3 Count changed for item 2, POSTing (id=2, count=6) to database.
4 (useEffect runs twice) Count hasn't changed yet, so don't update the DB (id=2, count=50)
第 3 行是不需要的行为:数据库将收到一个 POST,其中包含错误的项目 ID,理想情况下根本不应该发送。
这感觉像是一个 simple/common 问题:我应该使用什么设计来防止 POST useEffect 在陈旧状态下触发?所有我能轻易想到的解决方案在我看来都是荒谬的。 (例如,为每个项目创建一个 InternalComponent 并只显示其中一个,将所有 useEffects 组合成一个巨大的 useEffect 来跟踪组件中的每个状态等)我确定我忽略了一些显而易见的事情:有什么想法吗?谢谢!
好吧,你不是用它自己的 setter 函数更新 items
,而是将它置于 count
状态,这使得逻辑无处不在,非常困难阅读,老实说,我在这里提出一个可能解决您问题的不同解决方案:
const InternalComponent = ({ items, setItems, index }) => {
useEffect(() => {
console.log("item count changed: ", items[index].count);
}, [items, index]);
const handleButtonClick = () => {
setItems(
items.map((item) =>
item.id - 1 === index ? { ...item, count: item.count + 1 } : { ...item }
)
);
};
return (
<div>
I'm showing item {items[index].id}, which has count {items[index].count}.
<br />
<button onClick={handleButtonClick}>Increment item count</button>
</div>
);
};
此方法保持更新后的状态,如果您想在单击 next item
后默认返回其原始 count
值,应该很容易做到。
让我知道这种方法是否解决了问题
问题是由于使用 item
作为两个 useEffect
挂钩的依赖项引起的。当 item
属性更新时,第二个 useEffect
钩子被旧的 count
状态触发,然后在第一个 useEffect
钩子更新 count
之后再次触发第二次状态。
您基本上只想在项目道具更新时“重置”InternalComponent
状态。只需使用项目的 id
作为 InternalComponent
.
示例:
<InternalComponent key={item.id} item={item} />
然后密钥更改 React 丢弃(卸载)先前的实例并安装一个新实例,新实例已按预期初始化。
因为 InternalComponent
是 remounted/reset,第二个 useEffect
钩子现在完全无关,可以删除。状态值将采用新的 item
属性值。
const InternalComponent = ({ item }) => {
const [count, setCount] = useState(item.count);
const [countChanged, setCountChanged] = useState(false);
useEffect(() => {
if (countChanged) {
console.log(
`Count changed for item ${item.id}, POSTing (id=${item.id}, count=${count}) to database.`
);
} else {
console.log(
`Count hasn't changed yet, so don't update the DB (id=${item.id}, count=${count})`
);
}
}, [item, count, countChanged]);
const handleButtonClick = () => {
setCount(count + 1);
setCountChanged(true);
};
return (
<div>
I'm showing item {item.id}, which has count {count}.<br />
<button onClick={handleButtonClick}>Increment item count</button>
</div>
);
};
给 <InternalComponent />
一个唯一的 key
,所以它卸载并再次重新安装绝对是一个简单组件的方法,如@drew-reese[=15= 所述]
此外,如果我可以逃脱的话,我也支持对数据库或 API 进行 post 访问是一种有意识的行为。使它们成为 useCallback
中的显式操作(将计数、AND、post 添加到数据库)对于简单的更新场景来说是一个不错的选择。
至于为什么当你在示例代码中切换项目时,你得到不需要的 'POSTing' 到数据库,这只是因为你自己的错误,你没有将 countChange
重置为 false在你的代码中一旦你有 'POST' 它。
//...
useEffect(() => {
if (countChanged) {
//ADD THIS
setCountChanged(false);
console.log(
`Count changed for item ${item.id}, POSTing (id=${item.id}, count=${count}) to database.`
);
} else {
console.log(
`Count hasn't changed yet, so don't update the DB (id=${item.id}, count=${count})`
);
}
}, [item, count, countChanged]);
//...