conradkoh
conradkoh11mo ago

usePaginatedQuery never hits the cache (on the same client)

pagination is often used to reduce the number of rows scanned in the backend, and lazily load rows. Problem: When applying usePaginatedQuery, the cache is always missed when navigating between pages, that share a convex client. Example flow: 1. User loads app (convex context at root) 2. User goes to page 1 (load paginated query) 3. User navigates to page 2 (unmount paginated query) 4. User navigates to page 3 (load paginated query) - this misses the cache Expected result: Paginated query should serve from the cache, based on the page number, across the app. If users want to always bust the cache, they can add a parameter to args to do it. This also allows more granular control (e.g. embedding the screen name that the user is accessing from) to determine which screens share a cache and which screens don't. Reference: https://github.com/get-convex/convex-js/blob/250cf7985f5e69817e2b51d4558c166940b870c5/src/react/use_paginated_query.ts#L373-L400
GitHub
convex-js/src/react/use_paginated_query.ts at 250cf7985f5e69817e2b5...
TypeScript/JavaScript client library for Convex. Contribute to get-convex/convex-js development by creating an account on GitHub.
19 Replies
ballingt
ballingt11mo ago
Sounds like a new useCachePaginatedQuery() hook similar to the way we recommend paginating in scripts https://github.com/get-convex/convex-demos/blob/main/pagination/src/download.ts#L12-L30. Could you say more about "navigating between pages," what is being unmounted in steps 3 and 4? It sounds like you want to display only one page at a time — that's fine, can you still keep the other pages in memory? If not, you're dropping the subscription to them, but it's still possible to keep the previous query cached. How are you currently making that query, since the usePaginatedQuery doesn't expose the ability to unsubscribe to previous pages? It sounds a bit like you're using usePaginatedQuery in an unexpected way, seeing how you're using it might be helpful.
conradkoh
conradkohOP11mo ago
hey @ballingt, how are you. I was using pagination in a very simple way actually: https://github.com/conradkoh/baby-tracker/commit/7cd80f7a990e1d6afc53b2ac2b4b11b39f2d681a so just simply calling it from a page that mounts the query at a route in expo router. when I go to a different route, the page gets unmounted and the query loses a subscriber. when I come back, even if the data doesn't change, the cache is missed because of the id param added to args to the paginated query. so I'm not too sure about what the intended way to use usePaginatedQuery is. I am using it as a simple way to do useQuery but lazy load rows. so this is more an optimizaiton step for me. perhaps this is not the intended use?
conradkoh
conradkohOP11mo ago
GitHub
baby-tracker/apps/mobile/services/api.ts at master · conradkoh/baby...
Contribute to conradkoh/baby-tracker development by creating an account on GitHub.
ballingt
ballingt11mo ago
Ah got it, yeah when unmounting the component where the paginated query is indeed you'll lose these subscriptions. For now can you raise up the usePaginatedQuery to the first component that does not get unmounted? Or can you avoid doing a navigation that unmounts when the user clicks on the next page? You could still update the URL with the history api. There are two separate concepts here: dropping the subscription (causing at minimum the data to be resent from the server) and not getting cache hit when you query a second time. But not dropping the subscription will prevent submitting the same query again, eliminating the possibilitie of a cache miss. I would try to avoid unmounting the old query when the user changes pages. Ah my apologies @conradkoh now I get it:
when I go to a different route, the page gets unmounted and the query loses a subscriber. when I come back, even if the data doesn't change, the cache is missed because of the id param added to args to the paginated query
Is it fair to say this is a problem with queries in general, not specific to paginated queries? Since the reason it is not cached is that the id param has changed?
conradkoh
conradkohOP11mo ago
Is it fair to say this is a problem with queries in general, not specific to paginated queries?
I think it is specific to paginated queries though, because of these lines of code: https://github.com/get-convex/convex-js/blob/main/src/react/use_paginated_query.ts#L373-L400 this adds a pagination id to the args in the backend query (I found this by doing a console log). and this increments the pagination id every time a new subscription is added. so in general, paginations can never share state across loads on the same page, nor can they share across pages, because of this id. this is how I'm reading the code at least, please cmiiw!
ballingt
ballingt11mo ago
Oh ok, I think I'm caught up with you now, thanks for explaining. Indeed, this is a limitation of the usePaginatedQuery hook: we will do a fresh initial query each time. I would recommend staying subscribed as a workaround. You can't remain subscribed, because you want to make this query with different sets of arguments, right? I confused the term "pages" in your initial question with pagination pages
conradkoh
conradkohOP11mo ago
oh, my bad. actually, i am making sure that the input params are the same set of arguments, to try to hit the cache. but what I am observing is that the cache is still missed. in my code, the hook I shared is only used in 1 place (I extracted it to a hook so it would be easy to share here, not because I wanted to call it in different places haha). this is the only place I am using it: https://github.com/conradkoh/baby-tracker/blob/master/apps/mobile/app/index.tsx#L20-L23 explaining the code a little: I use the start of day from 7 days ago, to make sure that the input parameter is stable when we generate the ISO string (to try to hit the cache). but because of the id parameter generated by the usePaginatedQuery hook that is sent to args in the backend, this causes the cache to always be missed.
ballingt
ballingt11mo ago
yeah if you can stay subscribed that should fix it (because now there's just one ID) but I can understand that sometimes being difficult given different routing approaches If you're up for mucking around in the React code, all of it can be replaced with new custom hooks or no react all all; you could use the pagination logic to buidl your own query that doesn't live in a React component at all but practically I'd just keep moving this usePaginatedQuery up your component tree until you get to a componet that remains mounted, say the one right below where you convex client lives
conradkoh
conradkohOP11mo ago
mm right. do you think it would be possible to allow 0 as a valid input to the property initialNumItems, as I think this would solve the fundamental use case of lazy loading. regarding moving up the component tree - I think this is not favorable because most of the time it makes it kinda "global". but things start to get hairy when you have more than one place that wants different parameters to the same paginted query (especially in a relational model). for example in an accounting system, where there are different accounts, and each account has different transactions. if I want to have a hook like usePaginatedAccountTransactionHistory(accountId), it very qucikly becomes untenable to move this up because accountId would also need to be made global. I still feel that the internal ID parameter that auto increments will create a lot of issues in usability for developers. instead of internally managing the journals (hope I'm using the term correctly), I feel that the function should be pure. so as a user if I want all queries to share the same state, I will just use usePaginatedQuery(api.activityList, {}, { initialNumItems }) if I had 2 pages where I wanted a different state from one another, I would do usePaginatedQuery(api.activityList, { from: 'page-1' }, { initialNumItems }) usePaginatedQuery(api.activityList, { from: 'page-2' }, { initialNumItems }) this would ensure that they have different states. I have full control! just a suggestion though!
Michal Srb
Michal Srb11mo ago
Great suggestion, thanks @conradkoh!
ballingt
ballingt11mo ago
If you write your own hook that does this we'd love to see it! As you've seen usePaginationQuery is just one way to use the underlying pagination API.
conradkoh
conradkohOP11mo ago
hey @ballingt , I followed what you suggested. I duplicated the usePaginatedQuery hook and implemented a fix - confirmed that it fixed my issue. to be honest - I am not sure in which cases IDs should be different - I haven't dived deep enough into it. because of this lack of understanding, there might be a bug somewhere. https://github.com/conradkoh/baby-tracker/commit/2a6125b3535afa2e994d58d718f8ac1723af39e9 but at least for my issue, it seems to have fixed the caching problem, and it also provides a way to let the hook know deterministically which pages should share the cache and which ones should not, by providing a sharingKey parameter.
conradkoh
conradkohOP11mo ago
this is a screenshot of the before state - pagination is not hitting the cache on page reloads ever (even without the load more function being called).
No description
conradkoh
conradkohOP11mo ago
this is a screenshot of the after state.
No description
conradkoh
conradkohOP11mo ago
caches are getting hit 🙂
ballingt
ballingt11mo ago
That's great!
conradkoh
conradkohOP11mo ago
anyways, this is actually quite an interesting / important optimisation (for me) because of how database bandwidth is computed with the rounding to 1kb for each row. effectively, we get 1 mil free row reads on the free tier, with 1kb min per row, and 10GB free database bandwidth. on my simple app, with just 2 users (my wife and I), we're already hitting 20% of the free tier quota, and it hasn't even been the full month yet. so it makes me wonder if I could ever really share this app for people to try without factoring in the cost implications, as compared to something like planetscale with 1 billion row reads. of course the caching makes the computation slight different, I get that. just sharing what's on my mind! let me know your thoughts if you think the change makes sense! or if there was something I didn't factor in 🙂
ballingt
ballingt11mo ago
At minimum this should be an additional hook folks can use for this nicer behavior! I believe there is some difference to do with sharing multiple reactive paginated queries across an app? but I need to look deeper. Users ought to have a choice (and also maybe the default is wrong! we don't want to be charging for usage that isn't useful)
conradkoh
conradkohOP11mo ago
thanks @ballingt ! ☺️

Did you find this page helpful?