Skip to content

Commit

Permalink
feat(*) revalidate debounce, bugfix (Kong#9)
Browse files Browse the repository at this point in the history
* feat(*) add revalidateDebounce
* feat(*) export cache

Fixes Kong#8
  • Loading branch information
darrenjennings authored Feb 7, 2020
1 parent e6f016c commit 7f570fa
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 98 deletions.
63 changes: 58 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Features:
- [x] TypeScript ready
- [x] Minimal API
- [x] stale-if-error
- [x] Customizable cache implementation

With `swrv`, components will get a stream of data updates constantly and
automatically. Thus, the UI will be always fast and reactive.
Expand Down Expand Up @@ -104,6 +105,12 @@ const { data, error, isValidating, revalidate } = useSWRV(key, fetcher, options)
span
- `ttl = 0` - time to live of response data in cache
- `revalidateOnFocus = true` - auto revalidate when window gets focused
- `revalidateDebounce = 0` - debounce in milliseconds for revalidation. Useful
for when a component is serving from the cache immediately, but then un-mounts
soon thereafter (e.g. a user clicking "next" in pagination quickly) to avoid
unnecessary fetches.
- `cache` - caching instance to store response data in. See
[src/lib/cache](src/lib/cache.ts)
- `onError` - callback function when a request returns an error

## Prefetching
Expand All @@ -116,10 +123,7 @@ the SWRV cache at a predetermined time.
import { mutate } from 'swrv'

function prefetch() {
mutate(
'/api/data',
fetch('/api/data').then(res => res.json())
)
mutate( '/api/data', fetch('/api/data').then(res => res.json()) )
// the second parameter is a Promise
// SWRV will use the result when it resolves
}
Expand Down Expand Up @@ -152,7 +156,7 @@ export default {
setup() {
const endpoint = ref('/api/user/Geralt')
const { data, error } = useSWRV(endpoint.value, fetch)
return {
endpoint,
data,
Expand All @@ -162,3 +166,52 @@ export default {
}
</script>
```

## Cache

By default, a custom cache implementation is used to store both fetcher response
data cache, and in-flight promise cache. Response data cache can be customized
via the `config.cache` property.

```ts
import { SWRCache } from 'swrv'

class NoCache extends SWRCache {
get(k: string, ttl: number): any {}
set(k: string, v: any) {}
delete(k: string) {}
}

const { data, error } = useSWRV(key, fn, { cache: new NoCache() })
```

Leaving it up to the reader to implement their own cache if desired. For
instance, a common usage case to have a better _offline_ experience is to read
from `localStorage`.

## FAQ

### How is swrv different from the [swr](https://github.com/zeit/swr) react library?

#### Vue and Reactivity

The `swrv` library is meant to be used with the @vue/composition-api (and
eventually Vue 3) library so it utilizes Vue's reactivity system to track
dependencies and returns vue `Ref`'s as it's return values. This allows you to
watch `data` or build your own computed props. For example, the key function is
implemented as Vue `watch`er, so any changes to the dependencies in this
function will trigger a revalidation in `swrv`.

#### Features

Features were built as needed for `swrv`, and while the initial development of
`swrv` was mostly a port of swr, the feature sets are not 1-1, and are subject
to diverge as they already have.

## Authors

- Darren Jennings [@darrenjennings](https://twitter.com/darrenjennings)

Thanks to [Zeit](https://zeit.co/) for creating
[swr](https://github.com/zeit/swr), which this library heavily borrows from, and
would not exist without it as inspiration!
5 changes: 3 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ export interface IConfig {
refreshInterval?: number
cache?: SWRCache
dedupingInterval?: number
ttl?: number,
revalidateOnFocus?: boolean,
ttl?: number
revalidateOnFocus?: boolean
revalidateDebounce?: number
onError?: (
err: Error,
key: string
Expand Down
103 changes: 66 additions & 37 deletions src/use-swrv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import { IConfig, IKey, IResponse, fetcherFn } from './types'
const DATA_CACHE = new SWRCache()
const PROMISES_CACHE = new SWRCache()

const defaultConfig : IConfig = {
const defaultConfig: IConfig = {
cache: DATA_CACHE,
refreshInterval: 0,
ttl: 0,
dedupingInterval: 2000,
revalidateOnFocus: true,
revalidateDebounce: 0,
onError: (_, __) => {}
}

Expand Down Expand Up @@ -48,12 +49,13 @@ const mutate = async (key: string, res: Promise<any>, cache = DATA_CACHE) => {
* Stale-While-Revalidate hook to handle fetching, caching, validation, and more...
*/
export default function useSWRV<Data = any, Error = any> (key: IKey, fn: fetcherFn<any>, config?: IConfig): IResponse {
let unmounted = false
config = {
...defaultConfig,
...config
}

let keyRef = typeof key === 'function' ? (key as any) : ref(key)
const keyRef = typeof key === 'function' ? (key as any) : ref(key)

const stateRef = reactive({
data: undefined,
Expand All @@ -64,7 +66,8 @@ export default function useSWRV<Data = any, Error = any> (key: IKey, fn: fetcher
/**
* Revalidate the cache, mutate data
*/
const revalidate = async (keyVal = keyRef.value) => {
const revalidate = async () => {
const keyVal = keyRef.value
if (!isDocumentVisible()) { return }
const cacheItem = config.cache.get(keyVal, config.ttl)
let newData = cacheItem && cacheItem.data
Expand All @@ -78,57 +81,68 @@ export default function useSWRV<Data = any, Error = any> (key: IKey, fn: fetcher
/**
* Currently getter's of SWRCache will evict
*/
const promiseFromCache = PROMISES_CACHE.get(keyVal, config.dedupingInterval)
if (!promiseFromCache) {
const newPromise = fn(keyVal)
PROMISES_CACHE.set(keyVal, newPromise)
newData = await mutate(keyVal, newPromise, config.cache)
if (typeof newData.data !== 'undefined') {
stateRef.data = newData.data
}
if (newData.error) {
stateRef.error = newData.error
config.onError(newData.error, keyVal)
const trigger = async () => {
const promiseFromCache = PROMISES_CACHE.get(keyVal, config.dedupingInterval)
if (!promiseFromCache) {
const newPromise = fn(keyVal)
PROMISES_CACHE.set(keyVal, newPromise)
newData = await mutate(keyVal, newPromise, config.cache)
if (typeof newData.data !== 'undefined') {
stateRef.data = newData.data
}
if (newData.error) {
stateRef.error = newData.error
config.onError(newData.error, keyVal)
}
stateRef.isValidating = newData.isValidating
} else {
newData = await mutate(keyVal, promiseFromCache.data, config.cache)
if (typeof newData.data !== 'undefined') {
stateRef.data = newData.data
}
if (newData.error) {
stateRef.error = newData.error
config.onError(newData.error, keyVal)
}
stateRef.isValidating = newData.isValidating
}
stateRef.isValidating = newData.isValidating
}

if (newData && config.revalidateDebounce) {
await setTimeout(async () => {
if (!unmounted) {
await trigger()
}
}, config.revalidateDebounce)
} else {
newData = await mutate(keyVal, promiseFromCache.data, config.cache)
if (typeof newData.data !== 'undefined') {
stateRef.data = newData.data
}
if (newData.error) {
stateRef.error = newData.error
config.onError(newData.error, keyVal)
}
stateRef.isValidating = newData.isValidating
await trigger()
}

PROMISES_CACHE.delete(keyVal)
}

try {
watch(keyRef, (val) => {
revalidate(val)
})
} catch {
// do nothing
}

let timer = null
/**
* Setup polling
*/
let timer = null
onMounted(() => {
const tick = async () => {
// component might un-mount during revalidate, so do not set a new timeout
// if this is the case, but continue to revalidate since promises can't
// be cancelled and new hook instances might rely on promise/data cache or
// from pre-fetch
if (!stateRef.error && isDocumentVisible() && isOnline()) {
// only revalidate when the page is visible
// if API request errored, we stop polling in this round
// and let the error retry function handle it
await revalidate()
} else {
if (timer) { clearTimeout(timer) }
if (timer) {
clearTimeout(timer)
}
}
if (config.refreshInterval) {

if (config.refreshInterval && !unmounted) {
timer = setTimeout(tick, config.refreshInterval)
}
}
Expand All @@ -142,11 +156,26 @@ export default function useSWRV<Data = any, Error = any> (key: IKey, fn: fetcher
}
})

try {
watch(keyRef, (val) => {
keyRef.value = val
revalidate()
if (timer) {
clearTimeout(timer)
}
})
} catch {
// do nothing
}

/**
* Teardown
*/
onUnmounted(() => {
if (timer) { clearTimeout(timer) }
unmounted = true
if (timer) {
clearTimeout(timer)
}
if (config.revalidateOnFocus) {
document.removeEventListener('visibilitychange', revalidate, false)
window.removeEventListener('focus', revalidate, false)
Expand All @@ -159,4 +188,4 @@ export default function useSWRV<Data = any, Error = any> (key: IKey, fn: fetcher
}
}

export { mutate }
export { mutate, SWRCache }
Loading

0 comments on commit 7f570fa

Please sign in to comment.