-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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
useSuspenseQuery infinite refetch after SSR (Next.js App Router) #6116
Comments
you mean this occurs ONLY if you add a I don't think this ONLY occurs with |
@ojj1123 Updated to clarify: This also happens when I add a |
note that with v5, it's no longer allowed to pass
For some reason, the fix is to have at least one |
Yea, after playing around with it a bunch I concur this can resolve the infinite loop (though not when using My intuition (after trying and failing to get suspense working manually myself a number of times) is that, on the client side, React is running From the source code, it is clear that This StackOverflow answer provides a good explanation of the problem, but the solution is not exactly clear. How do we store the original promise without recreating it on each subsequent render without Here's how |
I don't fully understand that. If you don't want data fetching on the client, why useQuery? If you want it to live on the server exclusively - use server components and fetch in them directly ?
It shouldn't even throw on the client side, because if you fetch on the server and set a
yes, but this re-run doesn't trigger suspense again. Once data is in the cache, we never throw any promise again. So this can't be the problem imo |
Ok, very interesting, good callout. Perhaps it is something else then causing the infinite loop.
The use case I'm trying to solve for is essentially replicating data fetching similar to I understand there are tradeoffs to using this behavior (as voiced in this thread), but I think it is a valid use case and I'm hoping it can be effectively solved with Suspense / fetch-then-render. |
wouldn't you effectively get that if you have a suspense boundary with fallback=null in the root layout? I mean, there needs to be a suspense boundary somewhere for suspense to work (from a react perspective). I think next just abstracts that away a bit |
If you had With no suspense boundary at the application layer, the page waits until all the data is loaded to transition (I think?), which matches is the behavior with I'm not sure what the "expected" behavior is if you don't include a |
so you're talking about client side transitions? |
as far as I understand react, you must have a suspense boundary somewhere that catches the thrown promise. Otherwise, where would it go? I don't know what next does under the hood - my assumption is that they have a "global suspense boundary" anyways.
on the server or on the client ? for client side transitions, next router should wrap the navigation into a transition so that suspense boundary isn't triggered again |
I also found this problem when implementing SSR in nextjs 13. |
Hey @TkDodo, sorry for dropping off this thread! I'm back now with a demo of how the Next.js app directory handles the case when you don't define any suspense boundaries (either with
It seems that Next.js does handle this (it doesn't throw an error), but the behavior seems to be that Next.js blocks render during SSR and CSR if there is no suspense boundary defined. This is the behavior that I would expect / is the case that I previously demoed with Here is a very stripped down demo of this behavior as a branch on the same repo I made the original reproduction in. The demo shows how the streaming SSR waits for the suspense-based data fetch to finish before sending any data. I've removed DEMO: https://github.com/mgreenw/tanstack-query-suspense-bug/tree/basic-data-fetching-example I would expect I'd be happy to jump into a more realtime chat to discuss on Discord -- let me know! |
As I mentioned in the Discord, I think I've figured out what's going on here! I'd like to leave this issue open for now and we can close once I have a PR up with docs updates and the change to Problem
This can happen both on the server and on the client, but in this specific case it's the client that's causing the issue. In particular, this is the offending pattern: // NOTE: No suspense boundary above this
// Make the query client as recommended in the docs
const [queryClient] = useState(() => new QueryClient());
// This THROWS a promise if the data isn't available
const query = queryClient.useSuspenseQuery(); On the client, the call to Once the data fetching promise resolves, the tree is rendered again, and an entirely new SolutionThe solution is to only make a single I think this warrants an update to the Streaming SSR docs and examples in the repo. I'd be happy to open a PR to make these changes! Example of "use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
import { ReactNode } from "react";
function makeQueryClient() {
return new QueryClient({ /* ...opts */ });
}
let clientQueryClient: QueryClient | undefined = undefined;
function getQueryClient() {
if (typeof window === "undefined") {
// Server: always make a new query client
return makeQueryClient();
} else {
// Browser: make a new query client if we don't already have one
if (!clientQueryClient) clientQueryClient = makeQueryClient();
return clientQueryClient;
}
}
export const ClientProviders = ({ children }: { children: ReactNode }) => {
const queryClient = getQueryClient();
return (
<QueryClientProvider client={queryClient}>
<ReactQueryStreamedHydration>{children}</ReactQueryStreamedHydration>
</QueryClientProvider>
);
}; Other Learnings
In my initial reproduction, I was fully missing the
Once the HTML / data was streamed to the client, I was still seeing the client suspend once before it finally rendered the page. I wouldn't expect this because the client should already have all all the data it needs so it shouldn't need to suspend. I tracked it down to an issue with
This topic is not directly related to this issue, but is a real problem when we start using this pattern of data fetching with suspense in client components instead of using server components. In order to fetch data on the server with authentication, we usually need to pass along some client headers like This has been brought up a number of times in this discussion, and has also been discussed in this StackOverflow post. One potential solution is to use a server component to get the value of the A really clever workaround is to use the same method of grabbing the cookie in an RSC, but then instead of passing the cookie directly to the client component, we instead store the cookie value in a server-only global global data structure and pass a lookup key to the client component. When the client component needs the value, it can grab it from this server-only data structure (ideally using a client provider). Because we are only passing a lookup key between the RSC and client component boundary, we don't expose the actual cookie value in the rendered HTML. The main gotcha is that because these lookup keys and header values are only valid for the lifespan of the request, we need to clean them up in the global data structure once the request finishes (probably via a timer or TTL) so we don't infinitely grow memory usage. This technique has been successfully implemented in |
Works for me beautifully, thank you! |
thanks @mgreenw for the investigation and the thorough write-up ❤️ |
This has just saved me on a demo project I'm putting together for work! Absolute Hero! 🦸🏻 |
Works for me. Thank you! @mgreenw |
@mgreenw I would like to use this solution but I do not have a single If I understand correctly, your approach creates a global variable Is it ok to do that? Or should I create a separate global variable for every instance of |
@nizioleque In your case sounds like you'll need to create multiple global variables, one per provider instance. |
Describe the bug
After a page using
useSuspenseQuery
is SSR'd in the Next.js app router, the query will infinitely refetch. This also occurs if aloading.tsx
file is added.Your minimal, reproducible example
https://github.com/mgreenw/tanstack-query-suspense-bug
Steps to reproduce
pnpm i
pnpm dev
http://localhost:3000
Expected behavior
The query should only refetch at the expected times (window focus, mount, etc).
How often does this bug happen?
Every time
Screenshots or Videos
Platform
Tanstack Query adapter
react-query
TanStack Query version
5.0.0-rc.4
TypeScript version
5.2.2
Additional context
Found this first when fiddling with
trpc
in the Next.js app router. Seems to be affecting both v4 and v5.The text was updated successfully, but these errors were encountered: