Synchronize SWR Cache with IndexedDB to get offline cache.
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
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 interface
version
(optional): Schema version, defaults to 1
onError
(optional): Database error handler, defaults to noop functionNote: 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