IndexedDB Cache Provider for SWR
Synchronize SWR Cache with IndexedDB to get offline cache.
Examples of use cases include situations where you want to provide a fallback solution for fetch requests right after your app launches (when offline or with an unstable internet connection):
- Progressive Web Apps
- Capacitor Apps
Please note that the cache provider initialization process is asynchronous and until it's resolved, best practise is to render fallback component.
Warning
When passing secrets such as an API token to fetcher (example), it will be saved in the persistent cache as a part of key generated by stable-hash.
To overcome this, you may use custom comparision function in SWR Config that replaces secret by it's checksum.
Library reads current state of cache stored in IndexedDB into memory using idb during it's initialization. Then it resolves into Cache Provider which should be passed to SWR.
Read SWR Docs > Cache if your are interested in more information about implementation details.
Using npm:
npm install --save @piotr-cz/swr-idb-cache
or Yarn:
yarn add @piotr-cz/swr-idb-cache
- SWR ^2.0.0
Note: For SWR 1.x use the 1.0.0-rc.2 version of this package - Works with React ^16.11 and Preact
Initialize library and when it's ready, pass it in configuration under provider
key to SWR.
To wait until provider is resolved, use bundled useCacheProvider
hook:
// App.jsx
import { SWRConfig } from 'swr'
import { useCacheProvider } from '@piotr-cz/swr-idb-cache'
function App() {
// Initialize
const cacheProvider = useCacheProvider({
dbName: 'my-app',
storeName: 'swr-cache',
})
// Cache Provider is being initialized - render fallback component in the meantime
if (!cacheProvider) {
return <div>Initializing cache…</div>
}
return (
<SWRConfig value={{
provider: cacheProvider,
}}>
<Dashboard />
</SWRConfig>
)
}
…or library like react-use-promise:
import createCacheProvider from '@piotr-cz/swr-idb-cache'
import usePromise from 'react-use-promise'
function App() {
// Initialize
const [ cacheProvider ] = usePromise(() => createCacheProvider({
dbName: 'my-app',
storeName: 'swr-cache',
}), [])
// …
}
dbName
: IndexedDB Database namestoreName
: IndexedDB Store namestorageHandler
(optional): Custom Storage handler, see IStorageHandler interfaceversion
(optional): Schema version, defaults to1
onError
(optional): Database error handler, defaults to noop function
Note: When using useCacheProvider
, changing options doesn't create new cache provider.
InvalidStateError
Failed to execute 'transaction' on 'IDBDatabase': The database connection is closing.
See idb Issue #229
import useSWR, { useSWRConfig } from 'swr'
export default function Item() {
const { data, error } = useSWR('/api/data')
const { cache } = useSWRConfig()
const handleRemove = () => {
// Remove from state
// …
// Remove from cache with key used in useSWR hook
cache.delete('/api/data')
}
return (
<main>
{/** Show item */}
{data &&
<h1>{data.label}</h1>
}
{/** Remove item */}
<button onClick={handleRemove}>
Remove
</button>
</main>
)
}
Define custom storage handler that extends timestamp storage
// custom-storage-handler.js
import { timestampStorageHandler } from '@piotr-cz/swr-idb-cache'
// Define max age of 7 days
const maxAge = 7 * 24 * 60 * 60 * 1e3
const gcStorageHandler = {
...timestampStorageHandler,
// Revive each entry only when it's timestamp is newer than expiration
revive: (key, storeObject) =>
storeObject.ts > Date.now() - maxAge
// Unwrapped value
? timestampStorageHandler.revive(key, storeObject)
// Undefined to indicate item is stale
: undefined,
}
export default gcStorageHandler
Pass it to configuration
// App.jsx
import { SWRConfig } from 'swr'
import { useCacheProvider } from '@piotr-cz/swr-idb-cache'
+import customStorageHandler from './custom-storage-handler.js'
+
function App() {
// Initialize
const cacheProvider = useCacheProvider({
dbName: 'my-app',
storeName: 'swr-cache',
+ storageHandler: customStorageHandler,
})
// …
Define custom storage handler that extends timestamp storage
// custom-storage-handler.js
import { timestampStorageHandler } from '@piotr-cz/swr-idb-cache'
const blacklistStorageHandler = {
...timestampStorageHandler,
// Ignore entries fetched from API endpoints starting with /api/device
replace: (key, value) =>
!key.startsWith('/api/device/')
// Wrapped value
? timestampStorageHandler.replace(key, value)
// Undefined to ignore storing value
: undefined,
}
export default blacklistStorageHandler
Pass it in configuration as in the recipe above
-
Use mock IndexedDB
npm install --save-dev fake-indexeddb
// src/setupTests.ts import 'fake-indexeddb/auto'
or
-
Add mock cache provider with vitest
// src/App.test.tsx type SWRIdbCacheExports = typeof import('@piotr-cz/swr-idb-cache') vi.mock('@piotr-cz/swr-idb-cache', async (importOriginal): Promise<SWRIdbCacheExports> => { const mod = await importOriginal<SWRIdbCacheExports>() return { ...mod, useCacheProvider: () => () => new Map(), } })