T
TanStack•7mo ago
foreign-sapphire

SWR pattern within one query

I have two data sources for the same query/API endpoint. One cached, potentially stale but very quick to access, and another one, remote and fresh, etc. The idea is I would be OK with stale data but would refresh it in the background from the fresh data source. I wouldn't need to show a loading state as long as there's stale data around. The behavior I'd want on an empty QueryClient store: 1. Load from the fast data store 2. Render with it (contentful) 3. If the above response is stale, load from the remote data store 4. Re-render with the latest data And if I have data in the QueryClient already: 1. Render immediately using the most fresh version available, regardless of what data store it came from 2. If the data was stale, load from the remote data store 3. Re-render with the latest data On fetch error, I would want to know about the error state but also use the most recent version of data I have. I've been thinking about how to best realize this with react-query. I could make some custom useQuerySWR hook that would run two different queries (but always at least one enabled query) and mix in the output depending on what's available and if the cached data is stale. Or if I wanted to fold it into the same query from the perspective of react-query, I could possibly capture the QueryClient in my queryFn, return cached data from the promise but also spawn another promise against the fresh data source (if the cached version was stale) and call setQueryData directly. But then I'd run into trying to make this work with all the timing parameters and query state that react-query tracks. I've also considered placeholderData but I didn't grasp the full lifecycle of it, I think it gets reset when queryFn throws an exception? Is there a paved path to something like this? Ideally I'd just use a battle tested pattern and get the benefits for free, just like with everything else thanks to react-query 😄 .
5 Replies
dependent-tan
dependent-tan•7mo ago
you'd need to code that yourself, but it's not that hard to do within the queryFn, as we provide the queryClient and the queryKey to it::
queryFn: async ({ client, queryKey }) => {
const existingData = client.getQueryData(queryKey)
// first load - get from fast data store
if (!existingData) {
const data = await loadFromFastDataStore()
// if that data is not stale, return it
if (isFresh(data)) {
return data
}
// otherwise, write it to the cache
// this will put the query in success state and show data
// but it will still be in fetching state because the queryFn continues
client.setQueryData(queryKey, data)
}

// refetches and stale initial fetches land here
return loadFromRemoteDataStore()
}
queryFn: async ({ client, queryKey }) => {
const existingData = client.getQueryData(queryKey)
// first load - get from fast data store
if (!existingData) {
const data = await loadFromFastDataStore()
// if that data is not stale, return it
if (isFresh(data)) {
return data
}
// otherwise, write it to the cache
// this will put the query in success state and show data
// but it will still be in fetching state because the queryFn continues
client.setQueryData(queryKey, data)
}

// refetches and stale initial fetches land here
return loadFromRemoteDataStore()
}
You just need to know how isFresh(data) is going to look, I guess it means inspecting headers? Has nothing to do with react-query though
foreign-sapphire
foreign-sapphireOP•7mo ago
Thanks. So setQueryData would reset all the timers wrt staleTime, right? Anytime a queryFn resolves or setQueryData is called, it resets the age of that query? I'm thinking would it be possible to set a very high staleTime (like one hour) and then inspect the age of available data at the start of queryFn to decide where to route the request? Maybe something like
queryFn: async ({ client, queryKey }) => {
const existingData = client.getQueryData(queryKey)
const existingAge = client.getQueryAge(queryKey) ?? -1;
if (existingAge < 60*60) {
return loadFromFastDataStore()
}

return loadFromRemoteDataStore()
}
queryFn: async ({ client, queryKey }) => {
const existingData = client.getQueryData(queryKey)
const existingAge = client.getQueryAge(queryKey) ?? -1;
if (existingAge < 60*60) {
return loadFromFastDataStore()
}

return loadFromRemoteDataStore()
}
But then I'd need some method of telling QueryClient about the age of the data I just resolved from queryFn and I haven't seen anything like that. If what I'm describing could work, I think the advantage would be that react-query would re-fetch immediately when the data goes stale, right? For example: 1. store is empty 2. I fetch through the fast store and tell react-query this data is 45 seconds old 3. 15 seconds pass 4. react query calls my queryFn again, this time loading from the remote data store. If I don't have that level of control in react-query, then I'm probably going to rely on the browser cache/maybe a service worker and then react-query will refetch all the time but 99% of these would be served from the browser cache I found queryClient.getQueryState(queryKey).dateUpdatedAt but no way to set it, so I don't think my approach would work. Probably easiest to implement Cache-Control properly and let react-query attempt re-fetching periodically
dependent-tan
dependent-tan•7mo ago
So setQueryData would reset all the timers wrt staleTime, right? Anytime a queryFn resolves or setQueryData is called, it resets the age of that query?
yes because it's computed based on dataUpdatedAt, which gets set when data gets into the cache (no matter how)
I found queryClient.getQueryState(queryKey).dateUpdatedAt but no way to set it
you can set it with setQueryData:
queryClient.setQueryData(queryKey, newData, { updatedAt })
queryClient.setQueryData(queryKey, newData, { updatedAt })
It's there to tell react query: this data is of a certain age. It defaults to Date.now()
foreign-sapphire
foreign-sapphireOP•7mo ago
Oh very cool. But it is an undocumented API, so would it be OK to use it or is that as good as unstable & unsupported? https://tanstack.com/query/latest/docs/reference/QueryClient/#queryclientsetquerydata I've already implemented proper HTTP SWR semantics in my Cloudflare Worker, so react-query doing periodic refetches in the background is either served from the browser's cache or the edge's. But I might still use this API if it's stable. Could I also set the staleTime for that specific query in setQueryData?
QueryClient | TanStack Query Docs
QueryClient The QueryClient can be used to interact with a cache: tsx import { QueryClient } from '@tanstack/react-query' const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime:...
dependent-tan
dependent-tan•7mo ago
It's missing docs, please add it 🙂 It's very much supported You can't set staleTime because a query has no staleTime. Every observer can have a staleTime from its own point of view

Did you find this page helpful?