useMutableSource → useSyncExternalStore #86
Replies: 13 comments 27 replies
-
Thanks for the great news.
Wow, this is huge. This changes our strategy to design API. I have various questions, but I will read it again before making some of them. Meanwhile, the first question in mind is: does the shim support |
Beta Was this translation helpful? Give feedback.
-
Half-serious question: was it intentional that the abbrevation for this hook is going to change from |
Beta Was this translation helpful? Give feedback.
-
Is it correctly understood that if store update is in another micro task, it will deopt? const storeState = useSyncExternalStore(...);
const [state, setState] = useState();
const handleClick = () => {
setState(...);
Promise.resolve().then(() => setExternalStore(...));
}; |
Beta Was this translation helpful? Give feedback.
-
In case anyone's interested, I've got an initial prototype PR to migrate React-Redux's |
Beta Was this translation helpful? Give feedback.
-
This kinda sounds scary to me. I mean - this might remove all of the benefits of the time-sliced rendering for applications that relies heavily on external stores (such as Redux). I understand how it simplifies the model but it also feels like, for a lot of apps, time-slicing might become less relevant since a lot of updates will just be simply synchronous. I don't have any data but it feels like most of the "heavy" updates that could actually benefit from the time-slicing might come from external stores. |
Beta Was this translation helpful? Give feedback.
-
I'm having a little issue migrating to uSES. I dug into it and found where it comes from. The following patch fixes it. Does it make sense? --- src/useSyncExternalStoreExtra_orig.js 2021-10-04 08:44:01.000000000 +0900
+++ src/useSyncExternalStoreExtra_new.js 2021-10-04 08:44:44.000000000 +0900
@@ -76,7 +76,6 @@
}
// The snapshot has changed, so we need to compute a new selection.
- memoizedSnapshot = nextSnapshot;
const nextSelection = selector(nextSnapshot);
// If a custom isEqual function is provided, use that to check if the data
@@ -87,6 +86,7 @@
return prevSelection;
}
+ memoizedSnapshot = nextSnapshot;
memoizedSelection = nextSelection;
return nextSelection;
}; |
Beta Was this translation helpful? Give feedback.
-
Does this also apply to things like Media Queries or e.g. It seems like something similar to the following implementation would work for Media Queries function useMediaQuery(query: string, serverFallback?: boolean): boolean {
const getServerSnapshot =
serverFallback !== undefined ? () => serverFallback : undefined;
const [getSnapshot, subscribe] = useMemo(() => {
const mediaQueryList = window.matchMedia(query);
return [
() => mediaQueryList.matches,
(notify: () => void) => {
mediaQueryList.addEventListener("change", notify);
return () => {
mediaQueryList.removeEventListener("change", notify);
};
}
];
}, [query]);
return useSyncExternalStore(
subscribe,
// Fallback to getServerSnapshot only required for React 17
typeof window !== "undefined" ? getSnapshot : getServerSnapshot,
getServerSnapshot
);
} Example implementation and other examples (e.g. I think it would help if we could have a concrete example why we should use the Also: Should we update the initial post with the addition of |
Beta Was this translation helpful? Give feedback.
-
Salaam alaikum! I’m currently the point person for making sure Apollo Client works with React 18, and I’ve been asked by the React team in emails to post my thoughts and questions in the working group, with the idea that sharing our findings might lead to more meaningful social interactions with the wider React developer community. The current status for Apollo Client is that we have a PR which uses
As always, thanks for all the hard work, |
Beta Was this translation helpful? Give feedback.
-
Even today, reading from a stateful DOM API requires setting up a subscription. This isn't even specific to React; that's how other frameworks work, too. So one definition of a "mutable source" is anything that requires a subscription to notify React of changes. For example, a canonical example is a
Probably not. I would need to audit the Apollo implementation to be sure, but I would expect that you have a mutable store somewhere that the promises write to when they resolve. You would wrap subscriptions to that store in But the next step after that is to switch to a Suspense-based implementation. This is more complex topic, so I think a good next move would be to put you in contact with someone from the Relay team so they can provide advice on how they built their Suspense-based implementation. Let me know if you're open to this!
It's pretty easy to write a test that demonstrates a tearing bug, once you know what you're looking for. We have tons of them in the React test suite, including ones related to However, it's practically impossible to write a test that does the inverse: to exhaustively prove that a library/component contains zero tearing bugs. (Though I really wish there were — it would make my job easier!) To do that, you'd need to be able to prove that there are no unexpected side effects (reads or writes), which just isn't feasible in a highly dynamic language like JavaScript. You'd need some sort of formal, static verification like Rust's borrow checker. So our approach to testing for tearing bugs is adversarial: we intentionally try to break the UI, write a red/green test that simulates that particular scenario, then fix the behavior and merge the test. We don't have tests for every possible scenario, but we do aim to cover as many representative scenarios as we can think of. Libraries that build on top of React can use the same approach, of course, though our hope is that by using our official APIs, the scope issues they face is reduced: if React's APIs are thread-safe, and the libraries use React's API's in the recommended way, then those libraries should be thread safe, too. However, if you were find a tearing bug in React, that would be a highly valuable bug report! Because by fixing the issue in We already have a few examples of this happening:
|
Beta Was this translation helpful? Give feedback.
-
Oh! I didn't follow uSES fixes zombie children. cc @drcmda |
Beta Was this translation helpful? Give feedback.
-
Hi 👋 @gaearon kindly invited me to the WorkingGroup after a chat we had about uSES and react-query. I'm currently trying out migrating react-query towards uSES and would like to share my findings / thoughts with you:
Thank you for all the work on this - many things are working really well already with uSES and I'm really looking forward to v18 🎉 |
Beta Was this translation helpful? Give feedback.
-
This API is now available in the latest alphas (starting with 18.0.0-alpha-6bce0355c-20211031). |
Beta Was this translation helpful? Give feedback.
-
A small question when to use // a) normal hook from react 18
useSyncExternalStore(
store.subscribe, // this is stable
() => selector(store.getState()) // new reference in each render
)
// b) WithSelector from the package (not shim)
useSyncExternalStoreWithSelector(
store.subscribe, // this is stable
store.getState, // this is stable too
null,
selector, // new reference in each render
) Is it correct that b) can be theoretically more efficient than a)? Even if it's a micro-optimization. |
Beta Was this translation helpful? Give feedback.
-
Since experimental
useMutableSource
API was added, we’ve made changes to our overall concurrent rendering model that have led us to reconsider its design. Members of this Working Group have also reported flaws with the existing API contract that make it difficult for library maintainers to adoptuseMutableSource
in their implementations.After additional research and discussions, here are our proposed changes to the API, which we intend to complete for React 18.
(For background, refer to the RFC for
useMutableSource
: https://github.com/reactjs/rfcs/blob/master/text/0147-use-mutable-source.md)API overview
getSnapshot
is used to check if the subscribed value has changed since the last time it was rendered, so the result needs to be referentially stable. That means it either needs to be an immutable value like a string or number, or it needs to be a cached/memoized object.As a convenience, we will provide a version of the API with automatic support for memoizing the result of
getSnapshot
:Selectors no longer need to be memoized
Inline selectors are a common feature of state libraries, especially those that provide a hook-based API, such as Redux’s
useSelector
.A selector is a function that accepts a state value and “selects” the subset that is needed by the component. It signals that React only needs to re-render if that subset has changed.
Because
useMutableSource
does not provide a built-inselector
API, the only way to implement this behavior is to resubscribe to the store every time the inline selector changes. If the selector function is not memoized, this means resubscribing on every new render. This is not only a performance pitfall, it’s one that leaks into user code: the user of a state management library must take extra caution to memoize all of its selectors.Refer to #84 for a detailed description of this problem.
The new API is designed so that React no longer needs to resubscribe when the
getSnapshot
function changes. So you can pass an unmemoized selector without degrading performance.By default, React will detect changes to a selected value by comparing with
Object.is
. Some state libraries rely on comparing values with a custom comparison function, likeshallowEqual
. While we don’t necessarily recommend this pattern, it can be implemented in userspace. Since we expect this to be a common feature request, we will publish our own userspace implementation.Concurrent reads, synchronous updates
Another flaw of the
useMutableSource
API is that it can sometimes cause visible parts of the UI to be replaced with a fallback, even when the update is wrapped withstartTransition
(the API that is meant to avoid this scenario).The reason is that
startTransition
relies on the ability to maintain multiple versions of a UI simultaneously (“concurrently”): the current UI that’s visible on screen, and a work-in-progress UI that is prepared in the background while data progressively streams in. React can do this with its built-in state APIs —useState
anduseReducer
— but not for state that lives outside React, because we only can access a single version of state at a time. (To illustrate with an example, Redux’s store has agetState
method, but it doesn’t have agetBackgroundState
method; we could theoretically implement a contract to support concurrent data stores, but that’s outside the scope of this proposal.)Our original strategy was to provide partial support for concurrent features: start rendering concurrently, and only deopt back to synchronous when we detect an inconsistency. “Deopt" in this context can mean:
We went through great pains to preserve time-slicing as much as possible (1), even if it meant hiding already-visible UI with a fallback (2).
We’ve since concluded that this trade-off is backwards: Replacing visible content with a fallback is a significant regression in the user experience, especially if it happens unpredictably. By contrast, occasionally disabling time-slicing — while not ideal — has a much less dramatic effect on the end user experience.
Even worse is that
useMutableSource
can cause bad fallbacks during state updates that are completely unrelated, such as those triggered by a router, or a regularuseState
hook.As we were brainstorming alternative strategies, a key revelation was that if you can’t rely on
startTransition
to always avoid bad fallbacks caused by a store update, then you shouldn’t ever rely on it — you should avoid the fallbacks in some other way. For example, you could do it the same way you would today without Suspense: by waiting for new data to load before triggering the update.The next key revelation was that we can avoid deopts during updates triggered by React state transitions if updates triggered by external stores are always synchronous. That’s because if updates to stores are synchronous, they are guaranteed to be consistent.
So, in the new design:
startTransition
We think this hits the sweet spot of feature compatibility, ease of adoption, and predictable user experience.
We will be renaming the API to
useSyncExternalStore
to reflect its updated behavior.As a bonus, we can get rid of the
source
argument and the correspondingcreateMutableSource
API.Adoption strategy
Our goal is for all subscription-based libraries to migrate their implementations to
useSyncExternalStore
.While it’s possible to implement a concurrent-compatible subscription in userspace, it’s very tricky to get right. Existing store implementations will continue to work as they do in React 17 until or unless a store update is wrapped with
startTransition
, at which point concurrency bugs may surface.To encourage adoption by open source libraries, we will provide a shim that is compatible with older versions of React. The shim will prefer the built-in API when it is available, so that users get the correct implementation regardless of which version they’re running.
We are also considering a heuristic to detect when a userspace store update is wrapped with
startTransition
, so we can print advice to the console (in development mode) to useuseSyncExternalStore
instead. The heuristic is to count how many separate components are updated within a singlestartTransition
call. If it’s greater than some arbitrary threshold, say 20, we can infer that it likely contains a subscription.Beta Was this translation helpful? Give feedback.
All reactions