filipstefansson
filipstefansson14mo ago

useQuery local cache

Hi! I'm trying to figure out if there's any local caching with useQuery... I've setup a basic example where I make a query based on an ID in the URL and it looks like it's making a request to the backend every time: https://share.filip.sh/XBbZ2nxm. As you can see, the title is flashing on each route change and I log "log from db", which also happens every time. Frontend:
const thread = useQuery(api.threads.getBasic, {
threadId: props.params.threadId,
});
const thread = useQuery(api.threads.getBasic, {
threadId: props.params.threadId,
});
Backend:
export const getBasic = query({
args: { threadId: v.string() },
handler: async ({ db }, args) => {
const threadId = db.normalizeId("threads", args.threadId);
invariant(threadId, "id not found");

console.log("log from db");
const thread = await db.get(threadId);

return thread;
},
});
export const getBasic = query({
args: { threadId: v.string() },
handler: async ({ db }, args) => {
const threadId = db.normalizeId("threads", args.threadId);
invariant(threadId, "id not found");

console.log("log from db");
const thread = await db.get(threadId);

return thread;
},
});
I found this section in the docs, but it sounds like that's about caching on the server? https://docs.convex.dev/functions/query-functions#caching--reactivity
Queries | Convex Developer Hub
Queries are the bread and butter of your backend API. They fetch data from the
11 Replies
ballingt
ballingt14mo ago
Hi @filipstefansson, the caching in the Convex client is predictable but explicit: if you want to remain subscribed to a query even when not viewing the data you need to subscribe to the same query elsewhere. Getting more advanced, if you want to stay subscribed to previously loaded data or preload data you can create a Watch on a query and set a timer to release it. https://docs.convex.dev/api/interfaces/react.Watch
filipstefansson
filipstefanssonOP14mo ago
Ideally I'd like it to work like react-query, if you make the same query twice (with the same params), it returns the cached data first (from the first query) and then hits the backend. That would help with the flickering when going back to a previously visited page. But it sounds like I could achive that using Watch?
ballingt
ballingt14mo ago
That's right, either when your component renders or when you hover it you could subscribe and stay subscribed for an interval, here's a hover:
function onHover() {
const unsub = client
.watchQuery(api.foo.bar, {})
.onUpdate(()=>{});
setTimeout(unsub, 5000);
}
function onHover() {
const unsub = client
.watchQuery(api.foo.bar, {})
.onUpdate(()=>{});
setTimeout(unsub, 5000);
}
Where react-query maintains a cache of possibly inconsistent/stale data, Convex will only every show up-to-date data. You can either go explicit with Watch etc, using your knowledge of your app's information architecture to decide which queries to stay subscribed to, or you can write your own useStaleQuery hook that uses a global cache. Note re
if you make the same query twice (with the same params), it returns the cached data first (from the first query) and then hits the backend.
With Convex if you make two queries concurrently you will get this behavior, that's how the suggestion of staying subscribed by adding a Watch works: there is a global cache, it's just never stale. And instead of implicit LRU-style caching, it's explicit: you create Watches on data you want to stay subscribed to, and the React bindings automatically subscribe and unsubscribe when components are mounted and unmounted.
Michal Srb
Michal Srb14mo ago
Hey @filipstefansson check out the useStableQuery in this article: https://stack.convex.dev/help-my-app-is-overreacting It caches the result via useState, so if you're not unmounting your component you'll get no flash. Let us know if you are, we can help you write one that uses global in-memory store or SessionStorage.
Help, my app is overreacting!
Reactive backends like Convex make building live-updating apps a cinch, but default behavior might be too reactive for some use cases. Not to worry! L...
ballingt
ballingt14mo ago
@filipstefansson ☝️ Michal's solution here is slicker, check this out! It should be rare that you need the global cache that I spent 200 words talking about
jamwt
jamwt14mo ago
I think this might actually be the article @Michal Srb was referring to: https://stack.convex.dev/help-my-app-is-overreacting
Help, my app is overreacting!
Reactive backends like Convex make building live-updating apps a cinch, but default behavior might be too reactive for some use cases. Not to worry! L...
filipstefansson
filipstefanssonOP14mo ago
Since I'm switching routes, there's a full unmount when I leave the page and then a mount when coming back to the route, so it's not re-renders that's the problem I'm having. useStableQuery does a local cache, but I think I need a global cache. I named the thread "local cache" because I meant on the client and not on the server/db, so sorry for that confusion. I guess I could create a useStableQuery but one that writes to a global cache? In shory, my question is: Do you have anything like this planned:
// first arg is the query key
const result = useQuery(['thread', id], api.threads.get)
// first arg is the query key
const result = useQuery(['thread', id], api.threads.get)
or do we have to roll our own global cache solution?
ballingt
ballingt14mo ago
Ah, yeah if you want a global cache that can be stale you should roll your own. If you want a global subscription (like a cache but it stays up to date) that's the Watch approach, the query key used is api.threads.get combined with the the serialized arguments object {thread: id}. There are no immediate plans for a global cache that allows stale values but it's helpful to hear this is something you'd like. There are a variety of ways to manage this cache (when to expire, whether to refresh occasionally, how to evict due to memory limits) and there's no default we're providing because we prefer the subscriptions approach. I'd suggest trying subscriptions but if notes are changing quickly and you don't want to be notified of changes to notes not currently in view then the global stale cache seems appropriate.
Michal Srb
Michal Srb14mo ago
@filipstefansson here's roughly how to do it, instead of a ref you want a global object that survives your route navigation. The ConvexReactClient wrapping your app should be one such object:
// hooks/useStableQueryOverMounts.ts

import { useConvex, useQuery } from "convex/react";
import { FunctionReference } from "convex/server";

export const useStableQueryOverMounts = ((globalKey, name, ...args) => {
const result = useQuery(name, ...args);
const convex = useConvex();
const cache = ((convex as any).__cache__ ??= {});

// After the first render, stored.current only changes if I change it
// if result is undefined, fresh data is loading and we should do nothing
if (result !== undefined) {
// if a freshly loaded result is available, use the ref to store it
cache[globalKey] = result;
}

// undefined on first load, stale data while reloading, fresh data after loading
return cache[globalKey];
}) as QueryWithGlobalCacheKey;

type QueryWithGlobalCacheKey = <Query extends FunctionReference<"query">>(
globalKey: string,
query: Query,
...args: OptionalRestArgsOrSkip<Query>
) => Query["_returnType"] | undefined;

type EmptyObject = Record<string, never>;

type OptionalRestArgsOrSkip<FuncRef extends FunctionReference<any>> =
FuncRef["_args"] extends EmptyObject
? [args?: EmptyObject | "skip"]
: [args: FuncRef["_args"] | "skip"];
// hooks/useStableQueryOverMounts.ts

import { useConvex, useQuery } from "convex/react";
import { FunctionReference } from "convex/server";

export const useStableQueryOverMounts = ((globalKey, name, ...args) => {
const result = useQuery(name, ...args);
const convex = useConvex();
const cache = ((convex as any).__cache__ ??= {});

// After the first render, stored.current only changes if I change it
// if result is undefined, fresh data is loading and we should do nothing
if (result !== undefined) {
// if a freshly loaded result is available, use the ref to store it
cache[globalKey] = result;
}

// undefined on first load, stale data while reloading, fresh data after loading
return cache[globalKey];
}) as QueryWithGlobalCacheKey;

type QueryWithGlobalCacheKey = <Query extends FunctionReference<"query">>(
globalKey: string,
query: Query,
...args: OptionalRestArgsOrSkip<Query>
) => Query["_returnType"] | undefined;

type EmptyObject = Record<string, never>;

type OptionalRestArgsOrSkip<FuncRef extends FunctionReference<any>> =
FuncRef["_args"] extends EmptyObject
? [args?: EmptyObject | "skip"]
: [args: FuncRef["_args"] | "skip"];
Mathias
Mathias10mo ago
@Michal Srb Thank you for the provided useStableQueryOverMounts example, and sorry for just jumping in there. I have a question, as I'm trying to implement it in my app. I'm doing a to do app, where as I will have different boardIds. Should I include the boardId in the key, or is it enough like this:
const getBoard = useStableQueryOverMounts(
"getBoard",
api.boards.getTasksGroupedByColumns,
{
boardId: params.boardId,
}
)
const getBoard = useStableQueryOverMounts(
"getBoard",
api.boards.getTasksGroupedByColumns,
{
boardId: params.boardId,
}
)
Michal Srb
Michal Srb10mo ago
You should include the boardId in the key. You can change the hook implementation to do this automatically for you (you could even get rid of the custom key and generate the key from the function name and arguments, if you're ok persisting the same results from all callsites)

Did you find this page helpful?