This persistor extends QueryCache to persist the cache in a simple way without hydration/dehydration phase.
The biggest advantage in terms of performance is, that the persistor stores the queries by each single query instead of storing the whole cache on each change.
This is huge if your cache is several megabytes in size.
⚠️ The only drawback is that this only works in a synchronous way. You cannot use this if your storage only provides asynchronous methods to get/set the cache. See How it works
See below for examples.
import { PersistentQueryCache } from 'react-query-cache-persistent'
// Your implementation. See below for examples.
const persistentQueryCache = new PersistentQueryCache({
add: (query) => {},
updated: (query) => {},
removed: (query) => {},
})
const queryClient = new QueryClient({ queryCache: persistentQueryCache })
Uses localStorage
as storage.
import { PersistentQueryCache } from 'react-query-cache-persistent'
const persistentQueryCache = new PersistentQueryCache({
add: (query) => {
const item = window.localStorage.getItem(`queryCache.${query.queryHash}`)
if (item != null) {
query.state = JSON.parse(item)
}
window.localStorage.setItem(`queryCache.${query.queryHash}`, JSON.stringify(query.state))
},
updated: (query) => {
window.localStorage.setItem(`queryCache.${query.queryHash}`, JSON.stringify(query.state))
},
removed: (query) => {
window.localStorage.removeItem(`queryCache.${query.queryHash}`)
},
})
const queryClient = new QueryClient({ queryCache: persistentQueryCache })
Uses @op-engineering/op-sqlite
as storage. But you can use any sqlite solution as long it supports synchronous get/set.
The example below writes each query as a table row into a sqlite database
import {
ANDROID_DATABASE_PATH,
IOS_LIBRARY_PATH,
PreparedStatementObj,
open,
} from '@op-engineering/op-sqlite'
import { Query } from '@tanstack/react-query'
import { Platform } from 'react-native'
import { PersistentQueryCache } from 'react-query-cache-persistent'
const tableName = 'queryCache'
const connect = () => {
return open({
name: 'queryCache.sqlite3',
location: Platform.OS === 'ios' ? IOS_LIBRARY_PATH : ANDROID_DATABASE_PATH,
})
}
let db = connect()
db.execute(
`CREATE TABLE IF NOT EXISTS ${tableName} (queryHash TEXT NOT NULL UNIQUE, queryState TEXT) STRICT;`
)
const selectStmt = db.prepareStatement(`SELECT queryState FROM ${tableName} WHERE queryHash = ?;`)
const insertStmt = db.prepareStatement(
`INSERT INTO ${tableName} (queryHash, queryState) VALUES (?, ?) ON CONFLICT(queryHash) DO UPDATE SET queryState=excluded.queryState;`
)
const deleteStmt = db.prepareStatement(`DELETE FROM ${tableName} WHERE queryHash = ?;`)
/**
* Executes the prepared statement with the given parameters
*
* Will retry once if it throws `[OP-SQLite] DB is not open`
*/
const runStmt = (stmt: PreparedStatementObj, params: string[]) => {
try {
stmt.bind(params)
return stmt.execute()
} catch (error: unknown) {
if (`${error}`.includes('[OP-SQLite] DB is not open')) {
// retry once (on iOS the first execution fails on first start, but only in the context of PersistQueryCache::add)
db = connect()
stmt.bind(params)
const result = stmt.execute()
console.warn(`First execution failed. Second try worked.`)
return result
}
console.warn(`Failed to execute query: "${error}".`)
throw error
}
}
export const queryCache = new PersistQueryCache({
add: (query: Query) => {
const result = runStmt(selectStmt, [query.queryHash])
const firstRow = result.rows?._array[0]
if (firstRow != null) {
try {
query.state = JSON.parse(firstRow.queryState)
} catch (error: unknown) {
console.warn(`Failed to hydrate state for query "${query.queryHash}": ${error}`)
}
}
runStmt(insertStmt, [query.queryHash, JSON.stringify(query.state)])
},
updated: (query: Query) => {
runStmt(insertStmt, [query.queryHash, JSON.stringify(query.state)])
},
removed: (query: Query) => {
runStmt(deleteStmt, [query.queryHash])
},
})
TODO
MIT License.