我在显示单个数据项的组件(InternalComponent)中有两个 useEffect 挂钩。一个 useEffect 将计数作为状态进行跟踪,当用户增加计数时将 POST 到数据库。当 InternalComponent 跟踪的项目发生变化(由于外部操作)时,第二个 useEffect 会重置计数。
问题是:更新数据库的useEffect会在item改变时触发;它将看到新的项目值,但会错误地向数据库发送前一个项目的计数。发生这种情况是因为两个 useEffects “同时”触发,并且指示不应 POST 计数的状态直到 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>
);
};
代码沙盒上的最小工作示例: 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 行是不需要的行为:数据库将收到带有错误项目 ID 的 POST,并且理想情况下根本不应该发送该 POST。
这感觉像是一个简单/常见的问题:我应该使用什么设计来防止 POST useEffect 以陈旧状态触发?我能轻易想到的所有解决方案对我来说都是荒谬的。 (例如,为每个项目创建一个 InternalComponent 并仅显示其中一个,将所有 useEffects 组合成一个巨大的 useEffect 来跟踪组件中的每个状态,等等)我确信我忽略了一些明显的事情:有什么想法吗?谢谢你!
I have two useEffect hooks in a component (InternalComponent) that displays a single data item. One useEffect tracks a count as state, POSTing to a database when the user increments the count. The second useEffect resets the count when the item tracked by the InternalComponent changes (due to external manipulation).
The problem is: the useEffect that updates the database will fire when the item changes; it will see the new item value, but will incorrectly send the database the count from the previous item. This occurs because the two useEffects fire "simultaneously", with the state that would indicate the count shouldn't be POSTed not being set until after the POST useEffect is run.
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>
);
};
Minimal working example on Code Sandbox: https://codesandbox.io/s/post-with-stale-data-vfzh4j?file=/src/App.js
The annotated output:
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)
Line 3 is the unwanted behavior: the database will receive a POST with the wrong item ID and that ideally shouldn't have been sent at all.
This feels like a simple/common problem: what design am I supposed to use to prevent the POST useEffect from firing with stale state? All the solutions I can easily think of seem absurd to me. (e.g. creating one InternalComponent for each item and only displaying one of them, combining all the useEffects into a single giant useEffect that tracks every state in the component, etc.) I'm sure I'm overlooking something obvious: any ideas? Thank you!
发布评论
评论(3)
该问题是由于使用
item
作为两个useEffect
挂钩的依赖项引起的。当item
属性更新时,第二个useEffect
钩子会以旧的count
状态触发,然后在第一个useEffect 后再次触发
钩子更新count
状态。您基本上只想在 item 属性更新时“重置”InternalComponent 状态。只需使用项目的
id
作为InternalComponent
上的 React 键即可。示例:
然后关键更改 React 丢弃(卸载)以前的实例,并安装一个新实例,并按照您的预期初始化新状态。
由于
InternalComponent
已重新安装/重置,因此第二个useEffect
挂钩现在完全无关,可以删除。状态值将获取新的item
属性值。The issue is caused by using
item
as a dependency for bothuseEffect
hooks. When theitem
prop updates the seconduseEffect
hook is triggered with the oldcount
state, then again a second time after the firstuseEffect
hook updates thecount
state.You basically just want to "reset" the
InternalComponent
state when the item prop updates. Just use the item'sid
as a React key on theInternalComponent
.Example:
Then the key changes React throws away (unmounts) the previous instance and mounts a new instance with new state initialized as you expect.
Because
InternalComponent
is remounted/reset, the seconduseEffect
hook is completely extraneous now and can be removed. The state values will pick up the newitem
prop value.好吧,您没有使用自己的设置器函数更新
项目
,而是将其放在count
状态中,这使得逻辑到处都是,很难阅读,很难阅读,老实说,我在这里提出一个可能解决您问题的不同解决方案:如果您想默认回到它的原始
count> Count
值之后,此方法会保留更新的状态。 ,这样做应该很容易。让我知道是否这样解决问题
沙盒
Well, you are not updating the
items
with it's own setter function, instead put it in acount
state, which makes the logic all over the place, pretty hard to read, to be honest, I am here to propose a different solution that might solve your problem:This approach keeps the updated state, if you want to default back to it's original
count
value after clickingnext item
, it should be easy to do that.Let me know if this way solves the problem
Sandbox
正如 @drew-reese 所提到的,为
提供一个唯一的key
,以便卸载并再次重新挂载,这绝对是简单组件的最佳选择此外,如果我能逃脱惩罚的话,我还支持将发布到数据库或 API 视为一种深思熟虑的行为。对于简单的更新场景,使它们成为 useCallback 中的显式操作(在其中添加计数、AND、发布到数据库)是一个不错的选择。
至于为什么当您在示例代码中切换项目时会收到不需要的“POSTing”到数据库,这只是因为您自己的错误,您没有在代码中将
countChange
重置为 false 一次你已经“发布”了它。Giving
<InternalComponent />
a uniquekey
, so it unmount and mount fresh again is definitely the way to go for a simple component, as mentioned by @drew-reeseAlso I am proponent of making posting to database or APIs a deliberate action if I can get away with it. Making them a explicit action inside
useCallback
(where you add the count, AND, post to database) is a good option here for a simple update scenario.As for why you are getting unwanted 'POSTing' to database when you switch item in your sample code, it is just because of your own bug, that you did not reset the
countChange
to false in your code once you had 'POST' it.