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.
Here's a hypothetical implementation of such a function:
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;
}