useMutableSource and selector stability #84
Replies: 5 comments 35 replies
-
Thank you for putting together this detailed summary, Mark. 🙇🏼
if (
selector !== latestSelector.current ||
storeState !== latestStoreState.current ||
latestSubscriptionCallbackError.current
) {
const newSelectedState = selector(storeState)
// ensure latest selected state is reused so that a custom equality function can result in identical references
if (
latestSelectedState.current === undefined ||
!equalityFn(newSelectedState, latestSelectedState.current)
) {
selectedState = newSelectedState
} else {
selectedState = latestSelectedState.current
}
} else {
selectedState = latestSelectedState.current
} I know this comment may add more controversy to and already controversial thread, but I think it's important for the discussion to be complete and take all things into consideration. That being said, reading and writing from a ref during render like this is also unsafe. The rule of thumb for refs is:
|
Beta Was this translation helpful? Give feedback.
-
A minor clarification. It's not infinite loop, but didn't work as expected. Probably, I confused it with the behavior of |
Beta Was this translation helpful? Give feedback.
-
Thanks for this post! We're working on a proposal that I think will address your concerns here. Aiming to share it by the end of the week. The short version is: I do think we can remove the requirement that selectors need to be memoized by adding a Then Redux's implementation of function useSelector(selector) {
const store = useContext(ReduxStore);
return useMutableSource(store.subscribe, store.getState, {
// Or whatever the API ends up being
unstable_selector: selector,
});
} One thing we were hoping to avoid, though, is a So instead of const {a, b} = useSelector(state => ({a: state.a, b: state.b}), shallowEqual); users would write: const a = useSelector(state => state.a);
const b = useSelector(state => state.b); Is it feasible to remove support for To support migration, we could also add temporary support in 18, then remove in 19. But if we could drop support sooner rather than later that'd be great. As a bonus this could move Redux one step closer to being able to switch to a context-based implementation. |
Beta Was this translation helpful? Give feedback.
-
While I see uMS is mainly for library authors, I feel this api is confusing... It would become a technology debt. I have a question. Suppose, if we can afford to migrate to stable functions, can const getSnapshot = useMemo(() => {
let prevValue = null;
return (state) => {
const nextValue = selector(state);
if (equalityFn(prevValue, nextValuue)) {
return prevValue;
}
return (prevValue = nextValue);
}
}, [selector, equalityFn]); Or, does it include an issue with concurrent rendering? cc @acdlite @markerikson (created a different thread to avoid interrupting the discussion there.) |
Beta Was this translation helpful? Give feedback.
-
As an update for anyone reading this: the React and Redux teams just met to discuss issues around
|
Beta Was this translation helpful? Give feedback.
-
Background
The React team has created the
useMutableSource
hook as a compatibility mechanism to allow external state stores like Redux to avoid issues with "tearing". The behavior and implementation ofuseMutableSource
is described in these references:useMutableSource
(description)useMutableSource
(discussion)useMutableSource
A couple days ago, some concerns were raised on Twitter about the apparent need to memoize the creation of selector functions via
useCallback
, starting in this thread: https://twitter.com/TkDodo/status/1427922647401312257. I later got pulled into the discussion further down, starting at https://twitter.com/TkDodo/status/1428063106450743298 .The concern is that if a library migrates to using
useMutableSource
internally, all "selector" functions written by users of that library will now have to be wrapped inuseCallback
oruseMemo
to ensure that the selector functions have stable references. This appears to be necessary becauseuseMutableSource
relies on function reference changes as an indication that the current snapshot value needs to be recalculated, per the comment from Brian at https://twitter.com/brian_d_vaughn/status/1428076939466612740 :This pattern is actually already demonstrated in the hypothetical sample code in the RFC at https://github.com/reactjs/rfcs/blob/master/text/0147-use-mutable-source.md#example-user-component-code :
Daishi Kato has done some experimenting with
useMutableSource
, and in a proof of concept concluded that unstable selector references could cause a form of infinite loop: reduxjs/react-redux#1351 (comment) . However, he also just said he made a newer experiment and there may not have been as much of an issue: https://twitter.com/dai_shi/status/1428198999954132992 .Concerns
While this is a very understandable implementation approach, the need to memoize all selector functions appears to be a rather painful change to how libraries like React-Redux are currently used. For example, it's very common for selector functions to be defined as inline arguments to the
useSelector
hook, such as these typical snippets:This is especially true if the selectors reference values from props.
While it's not impossible for our users to wrap all selectors in
useCallback
, this would potentially be a sizeable breaking change for users who want to upgrade to React-Redux v8, as well as for users of other state libraries that have to go through similar changes - there's a lot of inline selectors in codebases out there.This is also a bit of a change conceptually, in that the current React docs have emphasized that "
useCallback
anduseMemo
are only there for perf optimizations". I do understand the implementation issues here, but it's definitely a shift in intended usage.For reference, React-Redux's
useSelector
hook also faces the same concern with new selector references changing. We deal with that by re-running the selector at render time if it's a new reference, but reusing the existing state value if the old and new values pass an equality check, per https://github.com/reduxjs/react-redux/blob/v7.2.4/src/hooks/useSelector.js#L31-L48 :Questions
With all that info, some questions:
useMutableSource
on each render?useMutableSource
so that it's more resilient to changing selector references?Tagging some interested parties for awareness:
@bvaughn , @gaearon , @TkDodo, @tannerlinsley, @phryneas , @timdorr, @dai-shi
Beta Was this translation helpful? Give feedback.
All reactions