-
-
Notifications
You must be signed in to change notification settings - Fork 96
fix: 🐛 Fix SSR prefetch by adding HydrationBoundary and stable cursor #44
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
base: main
Are you sure you want to change the base?
fix: 🐛 Fix SSR prefetch by adding HydrationBoundary and stable cursor #44
Conversation
fix: add HydrationBoundary and fix prefetch for data tables - Wrap all data tables in HydrationBoundary for proper SSR hydration - Add non-zero staleTime to prevent immediate client-side refetching - Fix infinite query initialPageParam to use stable cursor value - Pass server-parsed search params to client to avoid timestamp mismatches - Add loading states for better navigation experience Fixes prefetchQuery/prefetchInfiniteQuery not working without proper hydration setup The commit follows conventional commits format: - Type: fix (this is a bug fix - prefetch wasn't working) - Scope: Could optionally add scope like fix(data-table): if you prefer - Description: Clear and concise - Body: Lists the key changes - Footer: Explains what issue it fixes
@gruckion is attempting to deploy a commit to the OpenStatus Team on Vercel. A member of the Team first needs to authorize it. |
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.
One issue with using Date parsers in nuqs is that they all (server and client) have to be rooted in UTC to be consistent, and for the URL to have the same semantic meaning across the world.
This can (depending on your side of the UTC-0 line) be a bit jarring: parseAsIsoDate.parse('2025-09-22')
gives you the point in time at midnight, on the 22nd of September 2025 in UTC, so if you happen to be, say, in UTC+2 (like me as I write this), when you select a date from a UI set for local time, it actually renders the day before in the URL.
Timestamps indeed don't have this problem, both because they are explicitly rooted in UTC, and because they avoid human scrutiny.
queryKey: ["default-data", search], | ||
queryFn: async () => { | ||
// Use absolute URL for server-side fetching | ||
const baseUrl = typeof window === 'undefined' |
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.
suggestion: could you import the data directly here when in server-side (à la RSC), rather than going through HTTP server -> server? Or does it pose bundling separation issues?
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.
agree with you @franky47 - if it's a production app, I'd do a db query straight to get the data. Here, to keep the data consistent and change it only in a single place, I'd suggest to leave the http call.
@gruckion I think we would need to use the https://${VERCEL_URL}
on non dev environments to make it work.
we should probably do the same for the /infinite/query-options
to make it fetch from the server.
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The time range selectors don't work. On the default page:
On the infinite page, the selection works from the tooltip, but now when dragging from the bars on top of the table. |
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.
Hey @gruckion! appreciate it a lot. ❤️
While the default table works, I had issues with the infinite table. Left some comments while trying to debug it.
json, | ||
); | ||
}, | ||
initialPageParam: { cursor: new Date().getTime(), direction: "next" }, |
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.
good catch!
queryKey: ["default-data", search], | ||
queryFn: async () => { | ||
// Use absolute URL for server-side fetching | ||
const baseUrl = typeof window === 'undefined' |
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.
agree with you @franky47 - if it's a production app, I'd do a db query straight to get the data. Here, to keep the data consistent and change it only in a single place, I'd suggest to leave the http call.
@gruckion I think we would need to use the https://${VERCEL_URL}
on non dev environments to make it work.
we should probably do the same for the /infinite/query-options
to make it fetch from the server.
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.
the HydrationBoundary
doesn't seem to work on the infinite page for me.
when adding the absolute url to the query-options
- the isLoading property from useInfiniteQuery
always starts to be true - which shouldn't be the case.
I'm trying to debug it currently the dataOptions(search)
returns two different values based on server and client.
const search = searchParamsCache.parse(await searchParams); | ||
const search = await searchParamsCache.parse(searchParams); | ||
const queryClient = getQueryClient(); | ||
await queryClient.prefetchInfiniteQuery(dataOptions(search)); |
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.
when logging on server component (/infinite/page.tsx)
const queryOptions = dataOptions(search);
console.log('server key', queryOptions); // [ 'data-table', [ 'data-table', '' ] ]
await queryClient.prefetchInfiniteQuery(queryOptions);
while in client component (/infinite/client.tsx)
const queryOptions = dataOptions(search);
console.log('client key', queryOptions); // [ 'data-table', '?cursor=1758483849134' ]
const { .. } = useInfiniteQuery(queryOptions);
while search
is exactly same!
To be exact, the searchParamsSerializer = createSerializer(searchParamsParser)
returns two different values based on server and client.
@franky47 is that known?
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.
Yeah the problem is in the definition of the default value for cursor
in the search params definitions object: https://github.com/gruckion/data-table-filters/blob/3b564725b96f53d093567466eab64530f367f732/src/app/infinite/search-params.ts#L58
If you log the timestamp for this new Date()
, you'll see it differs between the client and the server, which makes sense because the code is evaluated at different times.
So on the server, the serializer sees the default value as equal (comparison by timestamp), but on the client it doesn't, and therefore renders it to the URL, giving you different React Query keys.
potentially non-related issues to the code changes 😅 - at least selecting a single data doesn't seem to work on prod |
Changes
data-table-filters/src/app/infinite/
<HydrationBoundary>
. Otherwise prefetching will not occur.staleTime
with non-zero value to thedataOptions
to prevent immediate refetching.initialPageParam
in query options to usesearch.cursor.getTime()
instead ofnew Date().getTime()
to ensure the pageParam matches between server prefetch and client hydration.loading.tsx
andSkeleton
component for better loading states during navigation (following the pattern from default table).data-table-filters/src/app/light/
staleTime
with non-zero value to thedataOptions
to prevent immediate refetching.<HydrationBoundary>
for proper hydration.data-table-filters/src/app/default/
Refactored to be consistent with the infinite scroll example, with async data fetching via the API route and non-zero
staleTime
for prefetching to not immediately refetch.DataTable
into it's ownClient
component for consistency with the infinite scroll example.dataOptions
to fetch the data via the API route with non-zerostaleTime
for prefetching to not immediately refetch.React.Suspense
in favour of Next.jsloading.tsx
Alex Sidorenko "Navigation feels slow in Next.js".HydrationBoundary
otherwise prefetching will not occur.