Preloading queries caching
Ok general question for those using NextJS w/ RSC.
I have routes that i'd like to pre-render and cache requests about my users in convex, but some of these are "forms" that the data might be updated live so I want that to be reactive.
I've seen in the docs that there is both
fetchQuery
and preloadQuery
and that you can then use the value from preloadQuery
. My goal for prerendering is two fold:
- faster inital page load for cached routes
- eliminate "jank" when data pops in, even if it's fast (like it would with useQuery
).
In my implementation, it doesn't seem like preloadQuery
is doing much except for stopping jank when I reload, and I know in the docs it says that preloadQuery
basically disables caching for the route as a whole.
My question/suggestion:
Wouldn't it be better to just fetchQuery
and then replace that response when a useQuery
comes in? We can still cache/invalidate as needed, and then just rely on useQuery
for real-time updates.
It would make it much easier for me to default to fetchQuery
and then just activate the "real-time" as needed.
I think this might go against some of the ways that the Convex team thinks about their product as whole, but I would interested in other users/maintainers feedback here. Am I thinking about these tools wrong?4 Replies
It sounds like you do want
preloadQuery
, but you don't want to bust the cache?
Is this really what you want though? The query fetch from convex should be very quick (10s of ms). If you don't bust the cache, the result could be very old, leading to jank as the result updates after the client subscribes to real-time values.
Were you planning to invalidate the Next.js fetch
cache?
---
Neither preloadQuery
nor fetchQuery
has an option to amend the cache behavior, but we could add such an option.
If you want to get something working immediatelly, you can write your own version of preloadQuery
using ConvexHTTPClient
. It's just a few lines of code (when you inline fetchQuery
and setupClient
).
https://github.com/get-convex/convex-js/blob/main/src/nextjs/index.ts#L98-L109GitHub
convex-js/src/nextjs/index.ts at main · get-convex/convex-js
TypeScript/JavaScript client library for Convex. Contribute to get-convex/convex-js development by creating an account on GitHub.
@Michal Srb I agree it's not really ideal, either way. Perhaps my question is better posed just as "what do you do" / "what do you recommend" as far as RSC goes.
Let's take a small example.
I have a page with some general content, and then I have my user's
<ProfileBadge />
in the top right corner, with a notification count. All the data needed can be found in one request, api.users.getMyUser
. Here is my understanding of render order for each of the options available to me, assuming that for each scenario I have added as much streaming/pending UI as possible (putting in a separate message for length limits)
with useQuery
1. SSR happens for this route. <ProfileBadge />
is a client component, so SSR doesn't included data fetching
2. Page loads, and assuming I have some pending UI, the pending UI is displayed.
3. useQuery
executes, and then replaces the pending UI.
4. When doing soft navigation, steps 2-3 are repeated.
PRO: Simple DX
CON: seo, jank
with preloadQuery
1. SSR happens for this route. <ProfileBadge />
is a RSC and fetches the data for this on the server, and the rest of the page is streamed in simultaneously.
2. Page begins to load, with the the suspense fallback in place for my <ProfileBadge />
3. <ProfileBadge />
finishes loading, gets to the client. usePreloadedQuery
is called and must resolve before data is shown (I think), unless preloadedQueryResult
is used to extract the value during RSC
4. When doing soft navigation, step 3 is repeated
PRO: seo
CON: DX, 2-hop requests every time
with fetchQuery & useQuery
1. SSR happens for this route. <ProfileBadge />
is a RSC and fetches the data for this on the server, and the rest of the page is streamed in simultaneously.
2. Page begins to load, with the the suspense fallback in place for my <ProfileBadge />
-- If cache exists, this is MUCH faster bc only 1-hop for the request
3. <ProfileBadge />
finishes loading, gets to the client. useQuery
is called after and can replace fetched data once it completes. No jank assuming cache is invalidated properly.
PRO: seo, DX (one fn call to use values during RSC instead of 2)
CON: DX, 1-hop requests with a properly managed cache.
As I was going through this, I might experiment with something like a fetchQuery
that utilizes the cache, and then a useQuery
variant that can register with convex to update values, and then will invalidate the RSC cache once the values diff. If I'm able to come up with anything I'll share it here for feedback.We currently recommend
useQuery
(convex-helpers
have client caching solutions that help with some jank during navigation), and where you need to (say for SEO), we recommend usePreloadedQuery
. You have correctly identified that the DX is worse for usePreloadedQuery
.
"with fetchQuery & useQuery" sounds just like usePreloadedQuery but with using the Next.js cache and invalidation. If you can make that work that'd be really interesting! I guess that the main win would not be from caching the data (since Convex already caches automatically), but from the fact that the whole Next.js request could be returned from the Next.js cache.Yeah I'm actually excited to try this out the more I think about it. Glad you agree it's an interesting approach!