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

Bug: React 18 hydration mismatch with sync external store #24388

Closed
tranvansang opened this issue Apr 16, 2022 · 9 comments
Closed

Bug: React 18 hydration mismatch with sync external store #24388

tranvansang opened this issue Apr 16, 2022 · 9 comments
Labels
Resolution: Expected Behavior Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug

Comments

@tranvansang
Copy link

The issue was brought from here #15074 (comment)

React version: 18.0.0

It is hard to mimic the hydration behavior with client-only code, I tried my best to reproduce the error here:

https://codesandbox.io/s/react-18-strict-mode-with-subscription-pattern-zfrfss?file=%2Fsrc%2FApp.js

Note about the demo: the error is actually different in the real SSR app (my app).

In my app (real SSR), error is "Warning: Did not expect server HTML to contain a <div> in <main>." error , while in the demo codesandbox, it is "This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.".

However, the behaviors are the same.

Error in the real SSR app.
image

Error in client-only SSR-mimic demo.
image

Test 1

data is empty, the server sends HTML with empty data immediately and the client fetches the data itself.

const LazyChild = lazy(async () => {
  await sleep(1000)
  return {default: ConsumeData}
})
const App = () => <>
  <ConsumeData/>
  <Suspense>
    <LazyChild/>
  </Suspense>
</>

Step 1: App is rendered, LazyChild is suspended
Step 2: ConsumeData fetches data (in an useEffect)
Step 3: data is fetched, and populated to the store
Step 4: LazyChild is resolved and rendered. But now it reads the fetched data. While in server, because the lazy component is always available, lazy components are rendered immediately and it reads empty data.

Test 2:

data is fully resolved in server, and populated to the store in the client before the hydration starts. StrictMode causes hydration mismatch.

const App = () => <StrictMode>
  <ConsumeData/>
  <Suspense>
    <ConsumeData/>
  </Suspense>
</StrictMode>

Step 1: data is populated to the store
Step 2: the tree is hydrated
Step 3: first render happens. Interestingly, ConsumeData is suspended even though it is not a lazy component.
Step 4: first render is destroyed
Step 5: data is unsubscribed. The store sees that there is no more subscription, it clears data to save memory
Step 6: second render reads data from memory. However, the data is now empty, and the hydration fails to match in the inner (the suspended) ConsumeData

In test 2, if Suspense OR StrictMode is removed from the tree, no error will occur.

Current behavior

Hydration fails with mismatches

Expected behavior

No error occurs, the hydration should work.

@tranvansang tranvansang added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label Apr 16, 2022
@tranvansang
Copy link
Author

tranvansang commented Apr 16, 2022

Can I have additional questions here?

With the current design, my data library can be easily converted to the immutable pattern, using useReducer/useState + useContext, and get rid of useSyncExternalStore.

However, every time the store gets a change, it notifies all listening useContexts.

My questions are:

  1. Regarding the performance, should I go with this pattern practically?
  2. Is it possible to listen to just a portion of a context. Something like preventing the re-render if the value does not change. Like this
const myData = useContext(StoreContext).myKey // if `myValue` referentially doesn't change. Is there any way to prevent re-rendering?
// something like useContext(StoreContext, 'myKey') would be very useful

I am not sure if useDeferredValue can make this possible.

Edited

These questions might be solved by https://github.com/dai-shi/use-context-selector

@eps1lon
Copy link
Collaborator

eps1lon commented Apr 20, 2022

The getServerSnapshot in useSyncExternalStore needs to return the original data that was used for rendering on the server not the current data:

 const getSnapshotServer = () => {
-  return data;
+  return SSR_DATA;
 };

Prior discussion with the same underlying issue: #22361

@eps1lon eps1lon closed this as completed Apr 20, 2022
@tranvansang
Copy link
Author

tranvansang commented Apr 20, 2022

The getServerSnapshot in useSyncExternalStore needs to return the original data that was used for rendering on the server not the current data:

 const getSnapshotServer = () => {
-  return data;
+  return SSR_DATA;
 };

Prior discussion with the same underlying issue: #22361

image

The issue still isn't fixed even I changed getSnapshotServer to return SSR_DATA.

@eps1lon eps1lon reopened this Apr 20, 2022
@hengwangsay
Copy link

hengwangsay commented Jun 15, 2022

same issue

@stanislav-halyn
Copy link

Same issue

@gaearon
Copy link
Collaborator

gaearon commented Jun 24, 2022

Please don't comment on issues with "same issue". This never helps anyone. It creates extra notifications but does not add any new valuable information. Instead, if you have a smaller reproducing example, please share it. Thanks!

@gaearon
Copy link
Collaborator

gaearon commented Jun 24, 2022

@tranvansang Can you please an SSR example? You can use Next.js in CodeSandbox. Or a standalone repo. Thanks!

@tranvansang
Copy link
Author

let me temporarily close this issue as it does not mimic the exact behavior of SSR with the latest react. I will re-open it when can make a reproduction

@gaearon
Copy link
Collaborator

gaearon commented Jun 24, 2022

Going to lock this one so that we don't get more mistaken comments. I have a hunch that "same issue" comments are really about legit hydration errors, but people google them and find this issue and assume it has something to do with their problem (which it most likely doesn't).

@facebook facebook locked as resolved and limited conversation to collaborators Jun 24, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Resolution: Expected Behavior Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug
Projects
None yet
Development

No branches or pull requests

5 participants