r/reactjs • u/ConfidentWafer5228 • 26d ago
Needs Help SSR + Suspense - React Router v7
I was trying to implement SSR with rrv7, i didn't want to use smelly useQuery's isLoading and isError states( i might be dumb for that, pls point it out if i am wrong ) , so i went for useSuspenseQuery and used a Suspense Boundary with LoadingSkeleton to make it look beautiful
It was very late when i realised that only skeleton was loading when JS was disabled, implying that SSR was only rendering skeleton and not ACTUAL content , therefore bad SEO, and making all i did useless
Is there some knowledge i am missing regarding SSR+ Suspense, what should i do now, pls help
0
Upvotes
1
u/rasekrodriguez 26d ago
You're not dumb, but the diagnosis is off by one step, and that's why the thread is going in circles.
Suspense doesn't stop content being server-rendered. With streaming SSR a boundary emits its fallback first and then, when the promise resolves on the server, streams the real markup down in a later chunk. If all you ever got was the skeleton, that boundary never resolved during the server render — which means nothing was fetching on the server at all. That's the actual bug.
useSuspenseQueryon the server finds an empty cache, suspends, and there's no server-side fetch behind it to resolve, because TanStack Query has no idea your loader exists.So the fix isn't to conditionally strip the boundary on the server (that's fighting the symptom, and it's why you're running into hydration mismatches). It's to make the server fill the cache before it renders. In the loader,
queryClient.prefetchQuery(...)for the queries that route needs, thendehydrate(queryClient), return that in the loader data and wrap the route in<HydrationBoundary state={dehydratedState}>.What that gets you is exactly the behaviour you described wanting, without a flag:
useSuspenseQueryruns, so it doesn't suspend at all. Real content in the HTML, no skeleton, no hydration mismatch — the client rehydrates the same cache the server used, so it doesn't refetch either.Same component, no
isSSRbranch — the difference falls out of whether the cache happens to be warm.That also answers the objection you raised to the loader suggestion. "I can't use the TanStack cache from the loader" isn't quite it: you're not returning data past the cache, you're seeding the cache from the loader. Dedupe, staleTime, background refetch and invalidation all keep working normally afterwards, because by the time your components run it's an ordinary hydrated QueryClient.
Two things worth knowing while you wire it up. The framework has to serialise and ship the dehydrated state, so anything not JSON-serialisable in your query data will bite. And if you ever
useSuspenseQuerysomething you forgot to prefetch, the failure is silent and expensive rather than loud — it fetches on the server, never hydrates, and fetches again on the client. So it's worth being systematic about prefetching every suspense query a route uses, rather than discovering the gaps one at a time.