-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
fix: no hydration when new promise comes in #8383
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
Changes from 23 commits
050c9d5
524f5ff
cf37452
d82cfb2
6eaf6fc
8c7ccf2
4efa642
f32f6e3
77362c4
5fa1a33
7eec72d
edda7ba
85ee5bc
4d9645b
a6d28f5
fb114b9
c5eccf5
6dbdf34
3c1b221
c691765
b7d9755
9d3b116
9ec623a
9ebf869
7584928
cef39db
a346f39
8448f7b
71a4453
9f555a2
4239947
9ef516c
a1d1c91
4c1dc96
8c2023e
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 |
---|---|---|
@@ -0,0 +1,11 @@ | ||
'use server' | ||
|
||
import { revalidatePath } from 'next/cache' | ||
import { countRef } from './make-query-client' | ||
|
||
export async function queryExampleAction() { | ||
await Promise.resolve() | ||
countRef.current++ | ||
revalidatePath('/', 'page') | ||
return undefined | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { countRef } from '../make-query-client' | ||
|
||
export const GET = () => { | ||
return Response.json({ count: countRef.current }) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,30 +1,41 @@ | ||
import { headers } from 'next/headers' | ||
import React from 'react' | ||
import { HydrationBoundary, dehydrate } from '@tanstack/react-query' | ||
import { Temporal } from '@js-temporal/polyfill' | ||
import { ClientComponent } from './client-component' | ||
import { makeQueryClient, tson } from './make-query-client' | ||
import { makeQueryClient } from './make-query-client' | ||
import { queryExampleAction } from './_action' | ||
|
||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) | ||
|
||
export default async function Home() { | ||
export default function Home() { | ||
const queryClient = makeQueryClient() | ||
|
||
void queryClient.prefetchQuery({ | ||
queryClient.prefetchQuery({ | ||
queryKey: ['data'], | ||
queryFn: async () => { | ||
await sleep(2000) | ||
const { count } = await ( | ||
await fetch('http://localhost:3000/count', { | ||
headers: await headers(), | ||
}) | ||
).json() | ||
|
||
return { | ||
text: 'data from server', | ||
date: Temporal.PlainDate.from('2024-01-01'), | ||
count, | ||
} | ||
}, | ||
}) | ||
|
||
const state = dehydrate(queryClient) | ||
|
||
return ( | ||
<main> | ||
<HydrationBoundary state={dehydrate(queryClient)}> | ||
<HydrationBoundary state={state}> | ||
<ClientComponent /> | ||
</HydrationBoundary> | ||
<form action={queryExampleAction}> | ||
<button type="submit">Increment</button> | ||
</form> | ||
</main> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,39 @@ | ||
// In Next.js, this file would be called: app/providers.tsx | ||
'use client' | ||
import { QueryClientProvider } from '@tanstack/react-query' | ||
|
||
// Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top | ||
import { QueryClientProvider, isServer } from '@tanstack/react-query' | ||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools' | ||
import * as React from 'react' | ||
import type { QueryClient } from '@tanstack/react-query' | ||
import { makeQueryClient } from '@/app/make-query-client' | ||
|
||
let browserQueryClient: QueryClient | undefined = undefined | ||
|
||
function getQueryClient() { | ||
if (isServer) { | ||
// Server: always make a new query client | ||
return makeQueryClient() | ||
} else { | ||
// Browser: make a new query client if we don't already have one | ||
// This is very important, so we don't re-make a new client if React | ||
// suspends during the initial render. This may not be needed if we | ||
// have a suspense boundary BELOW the creation of the query client | ||
if (!browserQueryClient) browserQueryClient = makeQueryClient() | ||
return browserQueryClient | ||
} | ||
} | ||
|
||
export default function Providers({ children }: { children: React.ReactNode }) { | ||
const [queryClient] = React.useState(() => makeQueryClient()) | ||
// NOTE: Avoid useState when initializing the query client if you don't | ||
// have a suspense boundary between this and the code that may | ||
// suspend because React will throw away the client on the initial | ||
// render if it suspends and there is no boundary | ||
const queryClient = getQueryClient() | ||
|
||
return ( | ||
<QueryClientProvider client={queryClient}> | ||
{children} | ||
<ReactQueryDevtools /> | ||
{<ReactQueryDevtools />} | ||
</QueryClientProvider> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -73,7 +73,10 @@ export const HydrationBoundary = ({ | |
} else { | ||
const hydrationIsNewer = | ||
dehydratedQuery.state.dataUpdatedAt > | ||
existingQuery.state.dataUpdatedAt | ||
existingQuery.state.dataUpdatedAt || | ||
// @ts-expect-error | ||
dehydratedQuery.promise?.status !== existingQuery.promise?.status | ||
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. this might actually be working 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. What if we prefetch a query without Maybe the entirely correct way to do this would be to inspect the data the query resolves to before determining whether to update the cache with that data? Implementation-wise that's a lot tricker though unfortunately. :( 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.
this is something only users could verify though. Without query meta-data like 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. Is it? If we are using the fetch timestamp from the server to determine this when hydrating data, we can surely do that for promises too, just after they resolved? I'm sure it might be a big painful thing to implement, but is there some inherent thing about user/library land that blocks us from doing this in the library using the same logic? To be clear, if we already have this query in the cache, this might require us to wait for the promise outside of the cache and only put the data in. Or even worse, to support fetching states properly, it might requires us to have some sort of "possibleUpdatePromise" or something that would not commit it's result to the cache if it's older. 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. @juliusmarminge the tests failures are happening because the tests hang indefinitely. Even locally, 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. How do you run the tests locally? When I run 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. it’s the 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. Oh - I wonder if that's cause we're not testing with the same type of promises that RSCs are sending down 🥹🥹 Can look later tonight when I get home 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. Fixed: 9ef516c |
||
|
||
const queryAlreadyQueued = hydrationQueue?.find( | ||
(query) => query.queryHash === dehydratedQuery.queryHash, | ||
) | ||
|
Uh oh!
There was an error while loading. Please reload this page.