Query cache in persistence storage #162
-
Hey @DamianOsipiuk, thanks so much for this project ! I was wondering if it there is a way to use some persistence capabilities already ? Apparently it is still experimental on the react query side. Cheers, |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments
-
Hi @gjeusel We could re-expose persistence packages from react-query, or at least mirror functionality, if dependency on react would be unavoidable. I will add that for a vue-query v2 roadmap. |
Beta Was this translation helpful? Give feedback.
-
I just want to add the remark that so far the persistence that I see being implemented is using localStorage. And localStorage, has, as far as I know, a limit of 5MB. I'm currently working on an app that will very likely surpass that 5MB by a lot. So I've chosen to write my own persistent cache implementation using indexedDB. Maybe it's worth considering to do the same in the official implementation? My implementation is ridiculously simple by the way, I only use one database with one table with one row. And that one row contains an object that contains my entire cache data. When my app loads, it loads the entire cache into a variable in memory and from that moment every And then my method I don't know if this is at all helpful or not, but I thought it might. In case you're curious about what the actual code looks like, here it is. (This is a spoiler object, just click here to open it and see the code.)// src/hooks/cache.ts
import IDBStore from "idb-wrapper"
import { throttle } from "lodash-es"
import { hashKey, QueryKey } from "./_utils"
const storeName = "SPKVStore"
interface Store {
get(): Promise<any>
set(value: any): Promise<void>
}
// store must be a promise, because it takes time
// for the store to actually be ready to use.
const store = new Promise<Store>(resolve => {
const store = new IDBStore({
storePrefix: "",
dbVersion: 1,
storeName,
onStoreReady() {
// When it is ready to use, I resolve the promise
// with the only two functions that I care about.
// get() just gets the entire cache data from the store.
// And set() saves / replaces the entire cache data in the store.
// As you can see, set() is throttled, because I want to make sure,
// that I don't overwhelm indexedDB when setCache() is called many times.
resolve({
get: (): Promise<any> => {
return new Promise((y,n) => store.get(storeName, y, n))
},
set: throttle((value: any): Promise<void> => {
value.id = storeName
return new Promise((y,n) => store.put(value, y, n))
}, 50, { leading: true, trailing: true })
})
}
})
})
// This is my in-memory cache,
// which I'll be using most of the time.
export const memCache = {}
export async function clearCache() {
// Delete all the keys from memcache.
for (const key in memCache) {
delete memCache[key]
}
// And then store the empty memcache
// object in the indexedDB store...
const { set } = await store
await set(memCache)
}
export async function setCache(key: QueryKey, data: any) {
// Every key is hashed, so the store is just
// a simple Record<string, any> dictionary.
const hashedKey = hashKey(key)
// If undefined was given as data, we'll assume
// the developer's intend was to delete the given key from the cache.
if (data === undefined) delete memCache[hashedKey]
// Otherwise we store both the data, and an updatedAt prop.
// The latter will come in handy when filling up vue-query's cache.
// To know whether the cache data should be considered
// fresh, stale, or just simply too old to even use.
else memCache[hashedKey] = { data, updatedAt: Date.now() }
// Again, save / replace the entire
// memCache variable in indexedDB
const { set } = await store
await set(memCache)
}
export function getCache(key: QueryKey): { data: any, updatedAt: number } {
const hashedKey = hashKey(key)
// As you can see, we're not asking the data from indexedDB,
// but just from the in-memory cache variable. This makes sure
// that this function can be synchronous and fast.
const value = memCache[hashedKey]
if (!value) return { data: undefined, updatedAt: 0 }
const { data, updatedAt } = value
const eightHours = 1000 * 60 * 60 * 8
// If the cached result is older than 8 hours, our arbitrary choice,
// we'll consider it too old, and so we'll just delete it and return undefined.
const result = Date.now() - updatedAt > eightHours ? undefined : data
if (result === undefined) setCache(key, undefined)
return { data: result, updatedAt }
}
// This function runs before the Vue app
// is even initiated. This is the one time
// we actually fetch data from indexedDB,
// to put it in our in-memory cache variable.
export async function fillMemCache() {
const { get } = await store
const data = await get()
Object.assign(memCache, data)
} And then I have one more file, which gets everything from // src/vue-query.ts
export function fillQueryDataFromPersistentCache() {
let count = 0
// Prepopulate query cache,
// to reduce the amount of queries done
// on a page refresh
Object.keys(memCache).forEach(key => {
// the "id" key is automatically by the idb-wrapper
// library I'm using, but I don't ever need it.
if (key === "id") return
// I keep track of how many entries were in the cache,
// because if it's not many, then I know our cache is incomplete
count++
const queryKey: QueryKey = JSON.parse(key)
const { data, updatedAt } = getCache(queryKey)
// Here vue-query uses updatedAt to determine the freshness of the data.
if (data) vueQueryClient.setQueryData(queryKey, data, { updatedAt })
})
return count
} |
Beta Was this translation helpful? Give feedback.
Hi @gjeusel
Persistence exports in react-query will become stable in v4, which is in beta right now (https://github.com/tannerlinsley/react-query/releases/tag/v4.0.0-beta.1).
Further plans is actually to separate react-query to a monorepo, so potentially we would be able to use these plugins from there. But that is planned for v5.
We could re-expose persistence packages from react-query, or at least mirror functionality, if dependency on react would be unavoidable.
I will add that for a vue-query v2 roadmap.
If you want to get that sooner, then feel free to open PR with it 😉