sbkl
sbkl9mo ago

preloadQuery client cache

Using the preloadQuery with nextjs app router, it is server rendering the page as expected. However, when I create a new element in a list that navigates to that page and I hit the back button, the list display is the initial page rendered missing the new item. Then the convex real time server kicks in and the new item appears. Recorded my screen for demo. Is there a way to trigger a refetch/invalidate of the preload query after a mutation similar to react-query?
3 Replies
Michal Srb
Michal Srb9mo ago
So there are two solutions: 1. Stay subscribed to the listing query (so you get realtime updates after the mutation, and when you navigate to the list page, the result is already on the client) 2. Refetch the server component: This is a Next.js question, maybe something with "tags" can do this? (I'm not sure) For the 1st approach: Some previous discussion on this topic: https://discord.com/channels/1019350475847499849/1210972532275023932/1211049107267780749 And relate custom hook: https://discord.com/channels/1019350475847499849/1185167649496367184/1186491636407087146 But the solution will really depend on your app.
sbkl
sbklOP9mo ago
So went for the watch solution and implemented a custom hook. As I am using the preloadQuery, I need to call both the preloadQuery and the useSubscribedQuery. Does that make sense and not pushing too many requests to the server?
export function useSubscribedQuery<Query extends FunctionReference<"query">>(
query: Query,
...args: ArgsAndOptions<Query, WatchQueryOptions>
) {
const client = useConvex();
React.useEffect(() => {
const unsub = client.watchQuery(query, ...args).onUpdate(() => {});
return function cleanup() {
// unsubscribe 5 seconds after unmount
setTimeout(unsub, 5000);
};
}, []);
}

/client component

"use client";

import { Preloaded, usePreloadedQuery } from "convex/react";
import { api } from "@/convex/_generated/api";

import { useSubscribedQuery } from "@/hooks/use-cached-query";

interface preloadedCollections {
preloadedCollections: Preloaded<typeof api.collections.list>;
}

export function CollectionList(props: preloadedCollections) {
const collections = usePreloadedQuery(props.preloadedCollections);
useSubscribedQuery(api.collections.list);
return (...)
}

/server component
import { redirect } from "next/navigation";

import { preloadQuery } from "convex/nextjs";
import { api } from "@/convex/_generated/api";

import { getAuthToken } from "../_lib/auth";

import { env } from "@/env";

export default async function Page() {
const searchParams = new URLSearchParams({
redirect_url: env.NEXT_PUBLIC_CLERK_REDIRECT_URL,
});
const token = await getAuthToken();
const preloadedCollections = token
? await preloadQuery(
api.collections.list,
{},
{
token,
},
)
: null;

if (!preloadedCollections) {
return redirect(`${env.NEXT_PUBLIC_CLERK_URL}?${searchParams.toString()}`);
}

return (...)
}
export function useSubscribedQuery<Query extends FunctionReference<"query">>(
query: Query,
...args: ArgsAndOptions<Query, WatchQueryOptions>
) {
const client = useConvex();
React.useEffect(() => {
const unsub = client.watchQuery(query, ...args).onUpdate(() => {});
return function cleanup() {
// unsubscribe 5 seconds after unmount
setTimeout(unsub, 5000);
};
}, []);
}

/client component

"use client";

import { Preloaded, usePreloadedQuery } from "convex/react";
import { api } from "@/convex/_generated/api";

import { useSubscribedQuery } from "@/hooks/use-cached-query";

interface preloadedCollections {
preloadedCollections: Preloaded<typeof api.collections.list>;
}

export function CollectionList(props: preloadedCollections) {
const collections = usePreloadedQuery(props.preloadedCollections);
useSubscribedQuery(api.collections.list);
return (...)
}

/server component
import { redirect } from "next/navigation";

import { preloadQuery } from "convex/nextjs";
import { api } from "@/convex/_generated/api";

import { getAuthToken } from "../_lib/auth";

import { env } from "@/env";

export default async function Page() {
const searchParams = new URLSearchParams({
redirect_url: env.NEXT_PUBLIC_CLERK_REDIRECT_URL,
});
const token = await getAuthToken();
const preloadedCollections = token
? await preloadQuery(
api.collections.list,
{},
{
token,
},
)
: null;

if (!preloadedCollections) {
return redirect(`${env.NEXT_PUBLIC_CLERK_URL}?${searchParams.toString()}`);
}

return (...)
}
Michal Srb
Michal Srb9mo ago
You can see how many fetches the client makes in your Convex dashboard on the logs page: https://dashboard.convex.dev/deployment/logs But the client will never fetch the same query with the same arguments twice, so this is a fine solution! If you look at how usePreloadedQuery is implemented you can make it even sleeker, by not requiring the client component to specify the query reference, but what you have is simple and explicit.
Convex Dashboard
Manage your Convex apps

Did you find this page helpful?