stcobbe
stcobbe3mo ago

Recommended pattern for initializing useQuery with server loaded value

Hey folks, Is there a recommended NextJS-compatible pattern for initializing a useQuery hook with a result from the server so that initial page load is very snappy but subsequent changes are fully reactive? (I realize this involves double fetching) e.g.
export default async function ChatServer({
chatId,
}: {
chatId: ChatId | null;
}) {
const initialMessages = chatId
? await fetchQuery(api.chats.listMessagesByChatId, {
chatId,
})
: undefined;
return (
<ChatClient
initialChatId={chatId}
initialMessages={initialMessages}
/>
);
}
export default async function ChatServer({
chatId,
}: {
chatId: ChatId | null;
}) {
const initialMessages = chatId
? await fetchQuery(api.chats.listMessagesByChatId, {
chatId,
})
: undefined;
return (
<ChatClient
initialChatId={chatId}
initialMessages={initialMessages}
/>
);
}
export default function ChatClient({
initialChatId,
initialMessages,
}: {
initialChatId: ChatId | null;
initialMessages?: ChatMessage[];
}) {
const messages = useQueryWithInit(
api.chats.listMessagesByChatId,
initialMessages,
chatId ? { chatId } : "skip",
);

return (
<>
<div className="w-full h-full flex flex-col">
<Content
messages={messages}
/>
</div>
</>
);
}
export default function ChatClient({
initialChatId,
initialMessages,
}: {
initialChatId: ChatId | null;
initialMessages?: ChatMessage[];
}) {
const messages = useQueryWithInit(
api.chats.listMessagesByChatId,
initialMessages,
chatId ? { chatId } : "skip",
);

return (
<>
<div className="w-full h-full flex flex-col">
<Content
messages={messages}
/>
</div>
</>
);
}
Here's a hypothetical implementation of such a function:
export function useQueryWithInit<T extends FunctionReference<"query">>(
queryFunction: T,
initialValue: FunctionReturnType<T> | undefined,
...args: OptionalRestArgsOrSkip<T>
): FunctionReturnType<T> | undefined {
const queryResult = useQuery(queryFunction, ...args);

const hasReceivedValue = useRef(false);

if (args[0] === "skip") {
return null;
}

if (queryResult !== undefined) {
// Mark that we've received a value
hasReceivedValue.current = true;
// Return the query result immediately
return queryResult;
}

if (!hasReceivedValue.current) {
return initialValue;
}

return undefined;
}
export function useQueryWithInit<T extends FunctionReference<"query">>(
queryFunction: T,
initialValue: FunctionReturnType<T> | undefined,
...args: OptionalRestArgsOrSkip<T>
): FunctionReturnType<T> | undefined {
const queryResult = useQuery(queryFunction, ...args);

const hasReceivedValue = useRef(false);

if (args[0] === "skip") {
return null;
}

if (queryResult !== undefined) {
// Mark that we've received a value
hasReceivedValue.current = true;
// Return the query result immediately
return queryResult;
}

if (!hasReceivedValue.current) {
return initialValue;
}

return undefined;
}
11 Replies
Convex Bot
Convex Bot3mo ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
jamwt
jamwt3mo ago
Next.js Server Rendering | Convex Developer Hub
Next.js automatically renders both Client and Server Components on the server
stcobbe
stcobbeOP3mo ago
This is exactly what I was looking for @jamwt. Appreciate the reply and sorry I missed this. Loving Convex, keep up the great work! Minor follow up question: any pointers (maybe a community implementation?) on how to implement a wrapper around usePreloadedQuery to support using React Suspense for implementing loading states? I'm struggling to adapt the useQuery example wrapper for Suspense for usePreloadedQuery . See: https://discord.com/channels/1019350475847499849/1295933266200891452
jamwt
jamwt3mo ago
hmm. @ballingt might have some ideas. he's defintely been pursuing this route on the tanstack start side
ballingt
ballingt3mo ago
We have some code for this, but what's the behavior you want here? You want to stream the promise for the preload from a server component, then resume live? I would probably either wait for the data on the server (which could cause a suspend, but that's not a convex hook thing) or just client side render if you want a loading state. This is possible but we want to be clear about the behavior you want
stcobbe
stcobbeOP3mo ago
@ballingt: Yea it's a little bit niche: my desired behavior is that the initial page load be preloaded with data that is fetched server side and waited on, and then resumes live after client side render (no loading UI in this case). However, for my use case, after client side render, certain actions can be taken that will result in fetching new data. It's in response to these client side actions that I want to display loading UI. I'm able to do this with result === undefined checks, but I prefer the Suspense / Boundary patterns, so I was hoping to have the option to somehow use usePreloadedQuery to achieve this. To summarize, I want the following behavior: 1. Preload data server side with await preloadQuery (no loading UI necessary) 2. Resume live client side with usePreloadedQuery 3. Support subsequent client side actions that refetch all data (display loading UI using Suspense ideally) To make it more concrete, I made a Loom video of the UI I'm working on: https://www.loom.com/share/05feb91800354a0396be12a0793a4d1a. In the video, the chat messages are initially preloaded with no loading state. But I want to display loading state when you select a new chat from the dropdown. Please let me know if I can provide any additional context. Appreciate the help here!
ballingt
ballingt3mo ago
That makes sense! We've prototyped this, blocking on the server but suspense on the client. The first thing we might do is expose a stable promise for the data having loaded so you can suspend yourself I'm not sure what to call this hook, it's a confusing behavior to describe but it makes sense that it's often what you'd want
stcobbe
stcobbeOP3mo ago
That sounds great, @ballingt! I know you're all super busy so I don't expect much here but any sense of when y'all might be able to get around to exposing such a promise?
ballingt
ballingt3mo ago
It's going to be a bit, likely this would be in convex-helpers or a community recipe first mostly because we'd like to try using it for a while before pushing people toward it I'm doing some work on TanStack Start examples and this may come up, but there we're using TanStack Query to do these pieces Have you seen existing suspense wrapper hooks for Convex? There's nothing in "convex/react" but there are community recipes if you want to play with this today using useSuspenseQuery() from TanStack Query is the simplest way today https://docs.convex.dev/client/tanstack-query This is a bit of work that it sounds like you've already looked at, it needs to replace the convex/react useQuery (rewrite some of the internals), it can't just wrap it
ballingt
ballingt3mo ago
GitHub
ents-saas-starter/lib/convex-gold-nextjs/index.tsx at gold · get-co...
Convex, Clerk, Next.js, Convex Ents. Contribute to get-convex/ents-saas-starter development by creating an account on GitHub.
ballingt
ballingt3mo ago
https://github.com/get-convex/ents-saas-starter/blob/e386710de96b0cd39cf5eb3c3ae95105ef2f7ae4/lib/convex-gold-nextjs/index.tsx#L31-L51 something like
const shouldSsr = typeof window === "undefined" && (options?.ssr ?? true);
const [ssrPromise] = useState(() =>
shouldSsr
? (async () => {
const data = await fetchQuery(query, args, { token: options?.token });
addInsertedServerHTMLCallback!(...);
return data;
})()
: undefined,
);
const ssr = shouldSsr ? use(ssrPromise!) : (window as any).__convexData![key];
const live = shouldSsr ? undefined : convexUseQuery(query, args);
return live ?? ssr;
const shouldSsr = typeof window === "undefined" && (options?.ssr ?? true);
const [ssrPromise] = useState(() =>
shouldSsr
? (async () => {
const data = await fetchQuery(query, args, { token: options?.token });
addInsertedServerHTMLCallback!(...);
return data;
})()
: undefined,
);
const ssr = shouldSsr ? use(ssrPromise!) : (window as any).__convexData![key];
const live = shouldSsr ? undefined : convexUseQuery(query, args);
return live ?? ssr;
@stcobbe it's helpful to hear this specifically requested! This is a pattern that makes sense. I'll probably implement it in TanStack Start first where we have more reliable data injection SSR primitives (although I think Next.js has added made these more stable now? I need to catch up)

Did you find this page helpful?