-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix useSelector race condition when dispatch happens in child's useLayoutEffect or componentDidUpdate #1532
Fix useSelector race condition when dispatch happens in child's useLayoutEffect or componentDidUpdate #1532
Conversation
Deploy preview for react-redux-docs ready! Built with commit 11db31a |
}) | ||
latestSelector.current = selector | ||
latestSelectedState.current = selectedState | ||
latestSubscriptionCallbackError.current = undefined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updating refs in the effect is intentional for concurrent mode.
Do you think this case can be solved by setting the initial ref values?
const latestSubscriptionCallbackError = useRef()
const latestSelector = useRef(selector)
const latestSelectedState = useRef(null)
if (latestSelectedState.current === null) {
// lazy ref initialization
latestSelectedState.current = selector(store.getState())
}
(But, this doesn't work in the case that selectedState can be null
. Should we use a special value?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updating refs in the effect is intentional for concurrent mode.
Do you think this case can be solved by setting the initial ref values?const latestSelector = useRef(selector) const latestSelectedState = useRef(selectedState)
Ah, is that so? I'm not yet aware of the interactions with concurrent mode. I'll try to provide a solution which keeps it in the useIsomorphicLayoutEffect
then.
Unfortunately, I don't think setting initial ref values would do anything in this context. The problem is that this block, which is logically updating the selectedState
cache to its newest value, is using a potentially stale variable. This can happen long after the initial render phase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can happen long after the initial render phase.
I see. I misunderstood the issue then. 😥
Can you provide a sandbox that demonstrates the bug before the fix? |
Uh... looking at this, no, moving the assignments into the render logic is not a correct solution. Mutations while rendering are never allowed, especially in Concurrent Mode. |
I'm also not convinced that the test case is demonstrating an actual bug. If you could explain the potentially bad sequence further (with a more realistic use case if possible), I'd appreciate it. |
Yep, here you go: ^In that sandbox, I'm simulating loading a user's configured views from a server. Each of those views, once loaded, needs to be initialized by the frontend. The Its parent, If you uncomment the first return in You may argue that this is a bad way to structure the components, which I completely agree with, but this is a real case example which shows that
Sounds good to me. I'll submit a fix soon which keeps the mutations wrapped in the |
That should go in a componentDidMount, not the update lifecycle. You're just creating an infinite loop, as you're not checking if the update is changing the There isn't a bug here, you're just using the lifecycle incorrectly. |
As I mentioned in my previous comment, I am aware that the example app I threw in the sandbox is using the lifecycle incorrectly. The point was that it's a very easy step to get If it is seen as intended behavior that P.S. For whoever may read this, the bug is also triggered when the child component is functional and dispatches in a |
That's what I thought with the first look (without deep look). |
Yee, here's the exact same sandbox except with a function component: |
Thanks. That helps me understand, and it's something I originally thought. (Though, my oversight was the assumption that it only happens in the initial render.) @timdorr I think this is a pitfall that can happen with useLayoutEffect (and cDM/cDU) in child components, if dispatching actions in useLayoutEffect is allowed. @laxels You might want to change the title as this is not only about class components. |
Done. Thanks for the reminder! |
👍 (BTW, I haven't yet come up with a solution.) |
@laxels diff --git a/src/hooks/useSelector.js b/src/hooks/useSelector.js
index d6e8592..10ff53e 100644
--- a/src/hooks/useSelector.js
+++ b/src/hooks/useSelector.js
@@ -22,15 +22,18 @@ function useSelectorWithStoreAndSubscription(
const latestSubscriptionCallbackError = useRef()
const latestSelector = useRef()
const latestSelectedState = useRef()
+ const latestStoreState = useRef()
+ const storeState = store.getState()
let selectedState
try {
if (
selector !== latestSelector.current ||
+ storeState !== latestStoreState.current ||
latestSubscriptionCallbackError.current
) {
- selectedState = selector(store.getState())
+ selectedState = selector(storeState)
} else {
selectedState = latestSelectedState.current
}
@@ -46,6 +49,7 @@ function useSelectorWithStoreAndSubscription(
latestSelector.current = selector
latestSelectedState.current = selectedState
latestSubscriptionCallbackError.current = undefined
+ latestStoreState.current = storeState
})
useIsomorphicLayoutEffect(() => { |
I filed a new PR #1536 as this PR is closed and hardly noticeable. |
Ah, just saw your comments @dai-shi. I'll continue this conversation in your new PR. |
When an action is dispatched inside a child class component's
useLayoutEffect
orcomponentDidUpdate
,checkForUpdates
synchronously executes before the code block inuseIsomorphicLayoutEffect
:This block sets
latestSelectedState.current
to theselectedState
value assigned by the latest render, which may be stale in comparison to theselectedState
value gotten incheckForUpdates
. In this case, the stale value overwrites the new value. I've added a test case which reproduces the issue.This PR fixes the issue by moving the ref-mutating lines out of
useIsomorphicLayoutEffect
, ensuring that they execute inuseSelector
's render step immediately. Executing during the render phase should be fine since all it's doing is updating refs to their most up-to-date value.