@piotr-cz/swr-idb-cache
v1.1.1
Published
IndexedDB Cache Provider for SWR
Downloads
2,244
Readme
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.
How does it work?
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.
Installation
Using npm:
npm install --save @piotr-cz/swr-idb-cache
or Yarn:
yarn add @piotr-cz/swr-idb-cache
Requirements
- 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
Setup
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',
}), [])
// …
}
Configuration
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.
Known issues
InvalidStateError
InvalidStateError
Failed to execute 'transaction' on 'IDBDatabase': The database connection is closing.
See idb Issue #229
Recipes
Delete cache entry
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>
)
}
Implement Garbage Collector
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,
})
// …
Ignore API endpoints from cache persistence
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
Mocking package in tests
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(), } })