Skip to content

Instantly share code, notes, and snippets.

@scarlac
Last active June 2, 2022 00:16
Show Gist options
  • Save scarlac/34f5bd183d849086654400367817f09b to your computer and use it in GitHub Desktop.
Save scarlac/34f5bd183d849086654400367817f09b to your computer and use it in GitHub Desktop.
Persist & Purge unused React Query query data from memory
// 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