Skip to content
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

selector in useSelector called one more time after forceUpdate inside hook #1697

Closed
oleggrishechkin opened this issue Mar 20, 2021 · 9 comments · Fixed by #1701
Closed

selector in useSelector called one more time after forceUpdate inside hook #1697

oleggrishechkin opened this issue Mar 20, 2021 · 9 comments · Fixed by #1701

Comments

@oleggrishechkin
Copy link

oleggrishechkin commented Mar 20, 2021

What is the current behavior?

When state changed, selector recomputes, and value sets into latestSelectedState. Component will re-render, and selector recomputes again, but its value already in latestSelectedState.current

(selector is arrow function - const data = useSelector((state) => state.data);)

What is the expected behavior?

Selector recomputes once

I can open PR to fix this, but I don't know, may be selector specially calls twice for some reasons

@markerikson
Copy link
Contributor

Can you put together a CodeSandbox that reproduces the issue?

@oleggrishechkin
Copy link
Author

Yes, try this https://codesandbox.io/s/react-redux-twice-selector-r43fy

I think calling selector twice may prevent stale selectedState if selector use data from other useSelector in the same component

@oleggrishechkin
Copy link
Author

oleggrishechkin commented Mar 24, 2021

@markerikson please, check CodeSandbox above

You can close this issue if it's correct behavior
I solve my own problem by memoize selector

const selector = useCallback((state) => some expensive selector, [some selector deps]);
const data = useSelector(selector);

It prevents selector call on re-render

@timdorr
Copy link
Member

timdorr commented Mar 24, 2021

@markerikson Do you happen to remember why it's implemented this way? We do two calls because one is run on the subscription callback to check if we need to re-render:

function checkForUpdates() {
try {
const newSelectedState = latestSelector.current(store.getState())
if (equalityFn(newSelectedState, latestSelectedState.current)) {
return
}
latestSelectedState.current = newSelectedState
} catch (err) {
// we ignore all errors here, since when the component
// is re-rendered, the selectors are called again, and
// will throw again, if neither props nor store state
// changed
latestSubscriptionCallbackError.current = err
}
forceRender()
}

If that's the case, then we re-render and the selector fires again to provide the value back to the component:

const newSelectedState = selector(storeState)

We could probably avoid that by also setting latestStoreState.current when running checkForUpdates. Was there any reason for not doing that? Some Suspense magic perhaps?

@markerikson
Copy link
Contributor

markerikson commented Mar 24, 2021

Yeah, I glanced at the repro briefly, and saw that the reason for the duplicate selector call is that latestStoreState.current is not being updated by the subscriber logic and is therefore failing the "has state changed" check when we re-render.

I'm not sure if it's an oversight, or a concern about mutating latestStoreState and ending up with weird CM-related shenanigans.

FWIW, I had a conversation with Dan yesterday where he suggested that "in practice most stuff 'just works' with CM" - ie, yes all the concerns about "tearing" that they've warned about are valid, but in practice it's not actually all that big of an impact.

Given that we are trying to track latestSelectedState, I can't immediately see why it would be bad for us to set latestStoreState too at the same time, which ought to resolve this. So, yeah, this seems like a one-line fix.

@oleggrishechkin
Copy link
Author

I miss latestStoreState change check, sorry, my "solution" above shouldn't work.
I updated CodeSandbox, now selector moved out of component

timdorr added a commit that referenced this issue Mar 25, 2021
Fixes #1697

We can avoid running the selector twice by setting the latestStoreState at the same time as setting the latestSelectedState. If the store state hasn't changed, then the selector won't as well (since it's pure).
@markerikson
Copy link
Contributor

I can try to put out a 7.2.4 in the next couple days.

@phantomcosmonaut
Copy link

Hey, I was using Suspense and a custom hook to render components asynchronously with async data fetching redux actions and I seem to have run into an issue directly related to this thread. I get the error Cannot update a component ('IdentityVerificationScreen') while rendering a different component ('RenderSuspender'). To locate the bad setState() call inside 'RenderSuspender' which takes me to the forceRender call from useSelector hook. Do you think the update in 7.2.4 will solve this issue and when can I expect to see the new version released? Thanks!

@markerikson
Copy link
Contributor

@phantomcosmonaut no, that shouldn't have anything to do with this issue. This is about a selector re-running when the component is already rendering. What you're describing is a store subscription causing a component to render in the first place.

If you're seeing problems with React-Redux and Suspense, please file a new issue with a reproduction so we can look into it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants