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
Bug: useSyncExternalStore does not schedule update after mutation #27670
Comments
If it would be useful, I can create a failing unit test from my Code Sandbox. (Edit: #27676) I'd also be happy to submit a PR with a proposed fix (but I'm less familiar with this hook's code in particular, so I might not be the best person to do this.) Either way, let me know if the above would be helpful. |
For more context see facebook/react#27670
If I’m not mistaken, this line is meant to catch if the store was mutated in between render and subscribe (if we were not already subscribed to the store at render time):
Perhaps this code isn’t running at the right time when reviving the tree. |
Agreed. I think that code only runs during mount or if the |
@acdlite are we missing a mutated value check here? |
Friendly ping. |
This is only a bug when used with If it's just with I'm still trying to write a test for it so that we can pick it up again at a later stage. |
The scope of the bug I've reported here requires Offscreen/Activity.
|
You can also repro this when you remove the function App({mode, revision}) {
return (
<React.unstable_Activity mode={mode}>
<Subscriber revision={revision} />
</React.unstable_Activity>
);
}
function Subscriber({revision: propRevision}) {
React.useEffect(() => {
console.log('store.set');
store.set('revision:' + propRevision);
}, [propRevision]);
const revision = useSyncExternalStore(store.subscribe, store.getState);
return <Text text={revision} />;
} If that passive effect is moved after the -React.useEffect(() => {
- console.log('store.set');
- store.set('revision:' + propRevision);
-}, [propRevision]);
const revision = useSyncExternalStore(store.subscribe, store.getState);
+React.useEffect(() => {
+ console.log('store.set');
+ store.set('revision:' + propRevision);
+}, [propRevision]); |
Here is a Replay recording with comments:
https://app.replay.io/recording/318114a4-3102-4732-ad1b-cb63b9c0ac22
I think the comments I've added show the following scenario:
Subscriber
) usesuseSyncExternalStore
to read from a mutable store.Subscriber
is hidden, and React unsubscribes from the store.Subscriber
component re-renders (with memoized state).Subscriber
component is not yet listening and so it does not re-render to reflect the updated store value.)Subscriber
component (useSyncExternalStore
) but it has already missed the mutation and React does not check for a changed snapshot value.We originally observed this behavior in Replay itself but I was able to reduce it to the following simplified case:
https://codesandbox.io/s/inspiring-bird-m4wv5l
I've added comments to the Replay, including ones that bracket the problematic
commitRoot
. Here's a short Loom as well talking through the bug:https://www.loom.com/share/2584cad5b4c44e6bba396ff8cf79db1d
I think an application could work around this issue in a couple of ways:
getSnapshot
andsubscribe
functions. (This would schedule some unnecessary effects work which is probably nice to avoid but maybe acceptable.)callback
React passes. (This would cause a lot of unnecessaryObject.is
comparisons but that's probably an acceptable cost.)I think the third option above seems best, but needing to do that feels like a foot gun for the API. Thoughts?
cc @acdlite, @sebmarkbage in case you find this interesting.
The text was updated successfully, but these errors were encountered: