T
TanStack3mo ago
afraid-scarlet

Client side cache best practices

Hi! Finally getting my hands on TanStack and I love it so far. One question I have though is regarding cache best practices when it comes to invalidate vs manually update it. Say I have a simple CRUD api:
GET /api/hospitals/[hospital-id]/services -> {serviceId, serviceName}[]
POST /api/hospitals/[hospital-id]/services

GET /api/hospitals/[hospital-id] -> {id, name, serviceCount}
PUT /api/hospitals/[hospital-id]
DELETE /api/hospitals/[hospital-id]

GET /api/services/[service-id] -> {id, name}[]
PUT /api/services/[service-id]
DELETE /api/services/[service-id]
GET /api/hospitals/[hospital-id]/services -> {serviceId, serviceName}[]
POST /api/hospitals/[hospital-id]/services

GET /api/hospitals/[hospital-id] -> {id, name, serviceCount}
PUT /api/hospitals/[hospital-id]
DELETE /api/hospitals/[hospital-id]

GET /api/services/[service-id] -> {id, name}[]
PUT /api/services/[service-id]
DELETE /api/services/[service-id]
The main thing from this API is that some queries say DELETE /api/services/[service-id] (DELETE_ SERVICE) will have some impact on the response of others e.g GET /api/hospitals/[hospital-id]/services(HOSPITAL_SERVICES) or GET /api/hospitals/[hospital-id](HOSPITAL). When using useMutation for the DELETE_SERVICE, I am not sure if manually updating the cache for the keys associated with HOSPITAL_SERVICES and HOSPITAL is a good practice. For the HOSPITAL_SERVICES cache I would need to loop through the array of {serviceId, serviceName} and remove the correct one. For the HOSPITAL cache I would need to decrement serviceCount by one. And to some extent one could argue that once I have the initial state of my app, then I could always rely on manually updating the cache and never actually refetc the data from the server. So my main question is to which extent is it considered good or bad practice? When invalidating cache is better over manually updating it? Another related question: is updating the cache "thread safe" (or async context is maybe more precise)? Not sure if it really means something in JS but basically if the same mutation is called twice and the onSuccess of both tries to decrease the cached serviceCount, isn't my state going to be corrupted (e,g showing 6 instead of 5 for instance)?
6 Replies
afraid-scarlet
afraid-scarletOP3mo ago
Last question is regarding query keys, how to best organize them? I know you can put structures into it and have some sort of hierarchy but when things are a little more coupled I don't really know what's best. For instance is there any pros/cons of doing this:
export const queryKeys = {
hospitals: {
root: ["hospitals"] as const,
byId: (hospitalId: string) =>
[...queryKeys.hospitals.root, "byId", hospitalId] as const,
services: (hospitalId: string) =>
[...queryKeys.hospitals.byId(hospitalId), "services"] as const,
},

services: {
root: ["services"] as const,
byId: (serviceId: string) =>
[...queryKeys.services.root, "byId", serviceId] as const,
},
};
export const queryKeys = {
hospitals: {
root: ["hospitals"] as const,
byId: (hospitalId: string) =>
[...queryKeys.hospitals.root, "byId", hospitalId] as const,
services: (hospitalId: string) =>
[...queryKeys.hospitals.byId(hospitalId), "services"] as const,
},

services: {
root: ["services"] as const,
byId: (serviceId: string) =>
[...queryKeys.services.root, "byId", serviceId] as const,
},
};
over
export const queryKeys = {
hospitals: {
root: ["hospitals"] as const,
byId: (hospitalId: string) =>
[...queryKeys.hospitals.root, "byId", hospitalId] as const,
},

services: {
root: ["services"] as const,
byHospital: (hospitalId: string) =>
[...queryKeys.services.root, "byHospital", hospitalId] as const,
byId: (serviceId: string) =>
[...queryKeys.services.root, "byId", serviceId] as const,
},
};
export const queryKeys = {
hospitals: {
root: ["hospitals"] as const,
byId: (hospitalId: string) =>
[...queryKeys.hospitals.root, "byId", hospitalId] as const,
},

services: {
root: ["services"] as const,
byHospital: (hospitalId: string) =>
[...queryKeys.services.root, "byHospital", hospitalId] as const,
byId: (serviceId: string) =>
[...queryKeys.services.root, "byId", serviceId] as const,
},
};
Thanks a lot for the help 🙏 Hey, bumping this, if anyone could help that'd be really appreciated
foreign-sapphire
foreign-sapphire3mo ago
Here some useful reads about best practices and patters of keys https://tkdodo.eu/blog/effective-react-query-keys as for caching, ideally you want to invalidate every time there a change, to make sure users sees actual server state (eg if another user deleted a service you will never know if you only patched the cache) The latency difference is barely noticeable in most cases but is much much easier than updating cache yourself over time. Updating the cache right away with data from a response and also invalidating the query for update with fresh data is common too (in case of lists changes beside you own updates for example)
Effective React Query Keys
Learn how to structure React Query Keys effectively as your App grows
afraid-scarlet
afraid-scarletOP3mo ago
Thansks for your answer. I definitely gave this blog post a read. I am concerned a bit with manually updating the cache. It looks like it is almost going to be re-implementing the backend logic to the frontend. Is it idiomatic in tanstack query? Also it couples different unrelated features like "services" and "hospitals". The services feature now needs to know all the other features that track data which could depend on a specific service and invalidate its cache. How is it done in practice when you have way more than 2 features? Cc @TkDodo 🔮 , sorry for the direct tag. I have not found this topic to he addressed a lot in your learning resources. I would love to hear your thoughts on that 🙏
foreign-sapphire
foreign-sapphire3mo ago
Take a look at the options of invalidateQueries, more often than not invalidating all the active queries after a mutation is a good tradeoff of performance and complexity https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientinvalidatequeries
foreign-sapphire
foreign-sapphire3mo ago
The idiomatic approach would be using query invalidation. Manual updates to the cache avoid another round-trip, but it means re-implementing server logic on the client.
afraid-scarlet
afraid-scarletOP3mo ago
Hey thanks for the answer, makes sense. I'm a bit concerned about this logic duplication between frontend and backend. I think that's certainly where the dev has to draw the line and sometine simply invalidate the query and let the frontend simply refetch the data

Did you find this page helpful?