Last active
June 2, 2022 00:16
-
-
Save scarlac/34f5bd183d849086654400367817f09b to your computer and use it in GitHub Desktop.
Persist & Purge unused React Query query data from memory
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Note that some disk-related and disk-auto-delete code has been removed | |
// PersistedQuery defines the JSON structure for the disk cache | |
type PersistedQuery = { | |
queryKey: QueryKey, | |
queryHash: string, | |
cacheTime: number, | |
state: { | |
data: unknown | undefined; | |
dataUpdatedAt: number; // Roughly same as 'last modified' date on filesystem | |
}, | |
}; | |
const queryClient = new QueryClient({ | |
defaultOptions: { | |
queries: { | |
staleTime: ONE_HOUR_IN_MS, | |
// cacheTime only affects in-memory cache, not on-disk, | |
// we want in-memory queries to be deleted once they are stale | |
cacheTime: 0, | |
}, | |
}, | |
}); | |
export const REACT_QUERY_CACHE_DIR = 'documents://react-query'; | |
function restoreDiskCacheIntoQuery( query: Query) { | |
const cacheKeyFilename = (query.queryKey); | |
const resolve = (rawJson: string|null) => { | |
if (rawJson === null) { | |
// No cache entry for the key, invalidate query so it re-fetches | |
queryClient.invalidateQueries(query.queryKey); | |
return; | |
} | |
const parsedJson: PersistedQuery = JSON.parse(rawJson); | |
query.cacheTime = parsedJson.cacheTime; | |
query.setState({ | |
...query.state, | |
...parsedJson.state, | |
}); | |
// Re-fetch if cached query was stale | |
queryClient.refetchQueries(query.queryKey, { stale: true }); | |
}; | |
const reject = (error: Error) => { | |
queryClient.invalidateQueries(query.queryKey); | |
}; | |
const cacheFilePath = `${REACT_QUERY_CACHE_DIR}/${cacheKeyFilename}.json`; | |
RNFS.exists(cacheFilePath).then(exists => { | |
// No cache on disk, mark query as invalid which triggers a server re-fetch | |
if (!exists) { | |
resolve(null); | |
return; | |
} | |
RNFS.readFile(cacheFilePath, 'utf8').then(resolve).catch(reject); | |
}).catch(reject); | |
} | |
function saveToDiskCache(query: Query) { | |
const cacheKeyFilename = (query.queryKey); | |
const newPersistedQuery: PersistedQuery = { | |
state: { | |
data: query.state.data, | |
dataUpdatedAt: query.state.dataUpdatedAt, | |
}, | |
queryKey: query.queryKey, | |
queryHash: query.queryHash, | |
cacheTime: query.cacheTime, | |
}; | |
const rawJson = JSON.stringify(newPersistedQuery); | |
return RNFS.writeFile(`${REACT_QUERY_CACHE_DIR}/${cacheKeyFilename}.json`, rawJson, 'utf8').catch(error => { | |
console.error('writeFile error', error); | |
}); | |
} | |
function usePersistedCache() { | |
useEffect(() => { | |
RNFS.mkdir(REACT_QUERY_CACHE_DIR).catch(error => { | |
console.error('mkdir error', error); | |
}); | |
const unsubscribe = queryClient.getQueryCache().subscribe(event => { | |
if (!event) return; | |
if (event.type === 'queryAdded') { | |
// Trick RQ into thinking 'undefined' is the most recent data, then | |
// once restoreDiskCacheIntoQuery finishes it'll invalidate or cause a re-fetch | |
event.query.state.data = undefined; | |
event.query.state.dataUpdatedAt = Date.now(); | |
event.query.state.dataUpdateCount = 1; | |
} | |
if (event.type === 'queryUpdated' && event.action.type === 'success') { | |
if (event.query.state.data) { | |
saveToDiskCache(event.query); | |
} | |
} | |
// Purge the payload from memory once we're done with it, | |
// above code will reload from disk when 'queryAdded' event happens | |
if (event.type === 'observerRemoved') { | |
if (event.query.getObserversCount() === 0) { | |
// If we remove the query while it's fetching any results that come in after won't get cached | |
// so instead we just clean out the major data and leave the metadata | |
event.query.state.data = undefined; | |
} | |
} | |
// We may already have the query metadata in memory, | |
// so an observer will be re-attached to it | |
// but the query.data will be undefined since we've purged it from memory, | |
// so restore it from disk | |
if (event.type === 'observerAdded') { | |
// Only restore from disk when the first observer gets attached | |
// (for maximum performance) | |
if (event.query.getObserversCount() === 1 && event.query.state.data === undefined) { | |
restoreDiskCacheIntoQuery(event.query); | |
} | |
} | |
}); | |
return () => { | |
unsubscribe(); | |
}; | |
}, []); | |
} | |
type Props = { | |
children: React.ReactChildren, | |
}; | |
function QueryClientProvider({ children }: Props) { | |
usePersistedCache(); | |
return ( | |
<QueryClientProvider client={queryClient}> | |
{children} | |
</QueryClientProvider> | |
); | |
} | |
export default QueryClientProvider; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment