-
Notifications
You must be signed in to change notification settings - Fork 47.2k
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
React 18: hydration mismatch when an external store is updated in an effect #22361
Comments
This is the purpose of the const selector = useCallback(() => store.getState().codeVariant, [store]);
return useSyncExternalStore(store.subscribe, selector, selector); Instead, the third argument should return the value used by the server: const getSnapshot = ()=> store.getState().codeVariant;
const getServerSnapshot = () => SERVER_STATE.codeVariant;
return useSyncExternalStore(store.subscribe, getSnapshot, getServerSnapshot); (Btw there's no performance advantage to these being memoized since they return immutable values.) How you implement SERVER_STATE will vary, but in the case of Redux, since it's an immutable store, you could do |
To explain how React uses this argument, it will call If something does change on the client, like in an effect, React will first hydrate the affected subtree, then apply the update on top of that. |
Because the store is actually the same on client and server as far as I can tell. But I don't know if any component in the tree might update the store (in a concurrent compliant way). Could you explain why my example works perfectly fine if I don't use Suspense? The server rendered and client rendered markup is the same with and without Suspense in my example. It just fails to hydrate once wrapped in Suspense. I'll play around with a different example to see how React handles it. But for me, the current uSES implementation breaks assumptions about composition. Now I have to tell every usage of the store that they can't update the store in an effect.
I don't understand how this is not what I'm already doing. It's just serialized in code instead of JSON since the store value is hardcoded:
|
I think I get it now. Even though I know that the server store and initial client store are the same, I don't know if the client store is still unchanged once a particular reader is getting hydrated. So I need to make sure I read from the initial copy. For my minimal example that's basically. function StoreRedux({ children }) {
const store = useMemo(() => {
return createStore(
(state = { value: "js" }, action) => {
if (action.type === "VALUE_SET") {
return { ...state, value: action.payload };
}
return state;
},
- { value: "js" }
+ { value: "js", server: { value: "js" } }
);
}, []);
function useValueRedux() {
const store = useContext(ReduxStoreContext);
const getSnapshot = useCallback(() => store.getState().value, [store]);
+ const getServerSnapshot = useCallback(() => store.getState().server.value, [
+ store
+ ]);
- return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
+ return useSyncExternalStore(store.subscribe, getSnapshot, getServerSnapshot);
} Edit: |
It's because React breaks up the hydration using the Suspense boundaries. So with the Suspense boundary, the effect hydrates and fires before the inner tree, causing a mismatch. Without the boundary, everything hydrates and commits in the same batch. |
Yeah we could consider that though maybe documenting it is good enough. |
Alright, thanks for clarifying 👍🏻 Closing since this an author error not library error. |
React version: #22347
Steps To Reproduce
useSyncExternalStore
) inside a Suspense boundary where no component suspendeduseSyncExternalStore
) outside of any Suspense boundaryuseEffect
)Link to code example: https://codesandbox.io/s/react-18-updating-store-in-an-effect-during-mount-causes-hydration-mismatch-uses-m6lwm?file=/src/index.js
The current behavior
<Demo />
inside the Suspense boundary causes a hydration mismatch since it's hydrated with the value set duringuseEffect
.The expected behavior
No hydration mismatch.
Repro explainer
The repro is based on reduxjs/react-redux#1794 which is based on a usage from the mui.com docs.
The behavior this repro is implementing is reading a value from
window.localStorage
(e.g. settings) with a fallback on the server.The
store
is a Redux store that is the same on Server and Client.Reading from the
store
is implemented like so:The repro contains an implementation that uses React Context for the store which works as expected i.e. no hydration mismatches.
Context
I recently stumbled over
-- #22277
which makes it sound like this behavior is expected because the update is inside a passive effect. Though it's unclear what is meant by "not encourage". How would I render a default value on the Server and populate it with the actually desired value on the Client?
The text was updated successfully, but these errors were encountered: