-
Notifications
You must be signed in to change notification settings - Fork 4
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
Fix hydration tests #2
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,9 @@ | ||
import React from 'react' | ||
import ReactDOM from 'react-dom' | ||
import ReactDOM, { Root } from 'react-dom' | ||
import ReactDOMTestUtils from 'react-dom/test-utils' | ||
import ReactDOMServer from 'react-dom/server' | ||
// eslint-disable-next-line import/no-unresolved -- types only for module augmentation | ||
import type {} from 'react-dom/next' | ||
|
||
import { | ||
useQuery, | ||
|
@@ -24,13 +26,12 @@ function setIsServer(isServer: boolean) { | |
const ReactHydrate = (element: React.ReactElement, container: Element) => { | ||
// @ts-expect-error | ||
if (String(process.env.REACTJS_VERSION || '18') === '18') { | ||
let root: any | ||
let root: Root | ||
ReactDOMTestUtils.act(() => { | ||
// @ts-expect-error | ||
root = ReactDOM.hydrateRoot(container, element) | ||
}) | ||
return () => { | ||
root?.unmount() | ||
root!.unmount() | ||
} | ||
} | ||
|
||
|
@@ -49,7 +50,7 @@ function PrintStateComponent({ componentName, result }: any): any { | |
return `${componentName} - status:${result.status} fetching:${result.isFetching} data:${result.data}` | ||
} | ||
|
||
describe.skip('Server side rendering with de/rehydration', () => { | ||
describe('Server side rendering with de/rehydration', () => { | ||
let previousIsReactActEnvironment: unknown | ||
beforeAll(() => { | ||
// @ts-expect-error we expect IS_REACT_ACT_ENVIRONMENT to exist | ||
|
@@ -96,6 +97,7 @@ describe.skip('Server side rendering with de/rehydration', () => { | |
'SuccessComponent - status:success fetching:true data:success' | ||
|
||
expect(markup).toBe(expectedMarkup) | ||
expect(fetchDataSuccess).toHaveBeenCalledTimes(1) | ||
|
||
// -- Client part -- | ||
const consoleMock = mockConsoleError() | ||
|
@@ -115,8 +117,8 @@ describe.skip('Server side rendering with de/rehydration', () => { | |
|
||
// Check that we have no React hydration mismatches | ||
expect(consoleMock).not.toHaveBeenCalled() | ||
expect(fetchDataSuccess).toHaveBeenCalledTimes(1) | ||
expect(el.innerText).toBe(expectedMarkup) | ||
expect(fetchDataSuccess).toHaveBeenCalledTimes(2) | ||
expect(el.innerHTML).toBe(expectedMarkup) | ||
|
||
unmount() | ||
consoleMock.mockRestore() | ||
|
@@ -177,12 +179,12 @@ describe.skip('Server side rendering with de/rehydration', () => { | |
|
||
// We expect exactly one console.error here, which is from the | ||
expect(consoleMock).toHaveBeenCalledTimes(1) | ||
expect(fetchDataError).toHaveBeenCalledTimes(1) | ||
expect(fetchDataError).toHaveBeenCalledTimes(2) | ||
expect(el.innerHTML).toBe(expectedMarkup) | ||
await sleep(50) | ||
expect(fetchDataError).toHaveBeenCalledTimes(2) | ||
expect(el.innerHTML).toBe( | ||
'ErrorComponent - status:error fetching:false data:undefined' | ||
'<!-- -->ErrorComponent - status:error fetching:false data:undefined' | ||
) | ||
|
||
unmount() | ||
|
@@ -240,12 +242,12 @@ describe.skip('Server side rendering with de/rehydration', () => { | |
|
||
// Check that we have no React hydration mismatches | ||
expect(consoleMock).not.toHaveBeenCalled() | ||
expect(fetchDataSuccess).toHaveBeenCalledTimes(0) | ||
expect(fetchDataSuccess).toHaveBeenCalledTimes(1) | ||
expect(el.innerHTML).toBe(expectedMarkup) | ||
await sleep(50) | ||
expect(fetchDataSuccess).toHaveBeenCalledTimes(1) | ||
expect(el.innerHTML).toBe( | ||
'SuccessComponent - status:success fetching:false data:success!' | ||
'<!-- -->SuccessComponent - status:success fetching:false data:success!' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These comment nodes have special meaning in React. I think the better assertion would use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you know, why React needs these comments? What is the purpose? As I understand, HTML-comments are ignored by React during hydration. And React uses it for its internal processes during selective hydration. Maybe you know more info about it?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That's pretty spot on. They have different markers for different markers. I know one type of marker is used for Suspense boundaries. This one my be for marking roots. But it doesn't really matter in the end for libraries. |
||
) | ||
|
||
unmount() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thank again, but one question: I really don't understand why the number of calls to the mocked callback would be different? That doesn't make much sense to me :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll take a look where this is called from.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added an additional assertion to make it clear that one call is during SSR and one in CSR.
I guess the second call during hydration is unexpected?
Though it seems like this is expected considering it originates from https://github.com/eps1lon/react-query/blob/b92b2113ad52783c2e7794248b93f22ed2fba171/src/core/queryObserver.ts#L107 which sounds like that fetching on mount is intended. Earlier tests probably didn't catch this because you didin't flush effects scheduled during hydration i.e. wrapped
hydrate
inact
.I noticed some other oddities with how you use
useSyncExternalStore
:I made that mistake as well originally. You really have to make sure that
getServerSnapshot
uses the server snapshot not the current snapshot i.e.getSnapshot
andgetServerSnapshot
cannot read the same value. React 18: hydration mismatch when an external store is updated in an effect facebook/react#22361 looks a lot like what you're doing. Does this make sense?That shouldn't be needed. That behavior should be implemented by
useSyncExternalStore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks again for looking into this. Yes, a refetch on mount is expected unless an explicit
staleTime
is set. This is actually mentioned right here in the docs, so I don't know how I didn't see it 🤦 . I'll probably add another test that makes sure we don't actually refetch on mount if we have a staleTime set, because I think that's missing.ad 1: Thanks for pointing that out, I think you are right, this is exactly what I'm doing, mainly because I have no idea how I would compute a server snapshot correctly 😔 .
ad 2: It's indeed a leftover from the previous implementation. Do you know if the shim will also have this behaviour implemented? Because if it's just falling back to useEffect / forceRender, I think we'd need that to still work with React 17.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also can't get the tests to run in v17, I think it's because of the uSES shim. What those tests do is they do something "on the server", then switch to client mode, render the same thing on the client and see if they match. However, the uSES shim imports two different versions (one for server, one for client). If I set the jest-environment to node, the client part fails because
document.createElement
doesn't exist. If I set it tojsdom
, the server part fails because the client version of the shim usesuseLayoutEffect
.we have this
setIsServer
function that tries to mock our internal utils so that RQ itself is in server/client mode, but I have no idea how to make the shim recognize that, too. Judging from the code, they also only check forwindow.document
, but mocking this insidesetIsServer
had no effect so far...https://github.com/facebook/react/blob/f320ef88f51fbdc5aba38bdf07108678a84f7339/packages/shared/ExecutionEnvironment.js#L10-L14
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can't really solve that with zero config. You probably have to go document something like
and then your
getServerSnapshot
reads from that globalreactQueryCache
.Though this approach doesn't work that well for streaming rendering I believe. May make more sense to switch a complete Suspense implementation using reactwg/react-18#25 in React 18. The core team was asking for feedback on their cache API so this might be a good opportunity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this point I'm just going to loop in @artem-malko who has written a proposal for :
But without going all that way, what else could we do to make it backwards compatible? Should
getServerSnapshot
inuseSyncExternalStorage
just returnundefined
for now?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can stick with the
getSnapshot === getServerSnapshot
approach to make it backwards compatible. But you have to be aware that this will lead to hydration mismatches with selective hydration ( see "React 18: Streaming HTML and Selective Hydration").Regarding caching I really shouldn't be talking about it and the new cache API since I haven't worked in either problem space beyond testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TkDodo Thx for the mention) I think, I'll create a separate test-case for SSR with renderToPipeableStream usage. So, I do not think, I will face with compatibility problems, cause I will use React 18 there. But, will see, how it will go)
@eps1lon, please, could you mention me, if you will find any cases of React.Cache usage in SSR streaming? This could help me a lot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an old discussion on a closed issue, but I'll post here anyway since it seemed a good place. 😄
About
getServerSnapshot
, this should be based off the initialqueryCache
like @eps1lon noted. When people do SSR with RQ today, this is already available in the form of thedehydratedState
(or rather the internal representation of thequeryCache
right after that state has been hydrated, before anything has changed). The way thedehydratedState
ends up on the client varies by framework, in the Next.js-demo it looks like this for example: https://github.com/TkDodo/react-query/blob/master/examples/nextjs/pages/_app.js#L10It might not always be that straightforward though, I see in the WIP PR that different things gets passed into
getSnapshot
in different places, for example() => observer.getCurrentResult()
. ThegetServerSnapshot
would have to be the same thing, but calculated from the initial cache so to speak.I noted this over in TanStack#2942 (comment) as well, but I think we should probably focus on the "everything is available before the client starts hydrating" case first before we start experimenting with the experimental Suspense cache. 😃
I plan to dive into this more and look at the WIP PR in more detail, but wanted to comment this right away.