Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/cache/adapters/localStorage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import SWRVCache, { ICacheItem } from '..'
import { IKey } from '../../types'

/**
* LocalStorage cache adapter for swrv data cache.
Expand Down Expand Up @@ -31,7 +32,7 @@ export default class LocalStorageCache extends SWRVCache<any> {
return undefined
}

set (k: string, v: any, ttl: number) {
set (k: IKey, v: any, ttl: number) {
let payload = {}
const _key = this.serializeKey(k)
const timeToLive = ttl || this.ttl
Expand Down
4 changes: 2 additions & 2 deletions src/cache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ export default class SWRVCache<CacheData> {
return serializeKeyDefault(key)
}

get (k: string): ICacheItem<CacheData> {
get (k: IKey): ICacheItem<CacheData> {
const _key = this.serializeKey(k)
return this.items.get(_key)
}

set (k: string, v: any, ttl: number) {
set (k: IKey, v: any, ttl: number) {
const _key = this.serializeKey(k)
const timeToLive = ttl || this.ttl
const now = Date.now()
Expand Down
18 changes: 15 additions & 3 deletions src/use-swrv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,15 @@ function resolveRetryFlag ({
return defaultConfig.shouldRetryOnError as boolean
}

function isSameSerializedKey (keyA: IKey, keyB: IKey, cache = DATA_CACHE): boolean {
return cache.serializeKey(keyA) === cache.serializeKey(keyB)
}

/**
* Main mutation function for receiving data from promises to change state and
* set data cache
*/
const mutate = async <Data>(key: string, res: Promise<Data> | Data, cache = DATA_CACHE, ttl = defaultConfig.ttl) => {
const mutate = async <Data>(key: IKey, res: Promise<Data> | Data, cache = DATA_CACHE, ttl = defaultConfig.ttl) => {
let data, error, isValidating

if (isPromise(res)) {
Expand Down Expand Up @@ -145,7 +149,7 @@ const mutate = async <Data>(key: string, res: Promise<Data> | Data, cache = DATA
// This filter fixes #24 race conditions to only update ref data of current
// key, while data cache will continue to be updated if revalidation is
// fired
let refs = stateRef.data.filter(r => r.key === key)
let refs = stateRef.data.filter(r => isSameSerializedKey(r.key, key, cache))

refs.forEach((r, idx) => {
if (typeof newData.data !== 'undefined') {
Expand Down Expand Up @@ -306,9 +310,14 @@ function useSWRV<Data = any, E = any> (...args): IResponse<Data, E> {
} else {
await mutate(keyVal, promiseFromCache.data, config.cache, ttl)
}
PROMISES_CACHE.delete(keyVal)

if (!isSameSerializedKey(stateRef.key, keyVal, config.cache)) {
return
}
Comment on lines +315 to +317

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Clear loading when stale request resolves after key is unset

When a request is in flight and the watched key changes to a falsy value (e.g. null), the watcher skips revalidate() and only updates isValidating, so isLoading can remain true. This new early return skips the only remaining isLoading = false path for that in-flight request, leaving consumers stuck in a perpetual loading state once the old promise resolves. This regression is triggered specifically by key transitions from truthy to falsy during an active fetch.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8660a69 by clearing isLoading when the watched key becomes falsy, with a regression test for the in-flight request case.


stateRef.isValidating = false
stateRef.isLoading = false
PROMISES_CACHE.delete(keyVal)
if (stateRef.error !== undefined) {
const configAllows = resolveRetryFlag({ shouldRetry: config.shouldRetryOnError, error: stateRef.error })
const optsAllows = resolveRetryFlag({
Expand Down Expand Up @@ -434,6 +443,9 @@ function useSWRV<Data = any, E = any> (...args): IResponse<Data, E> {
}
stateRef.key = val
stateRef.isValidating = Boolean(val)
if (!val) {
stateRef.isLoading = false
}
setRefCache(keyRef.value, stateRef, ttl)

if (!IS_SERVER && !isHydrated && keyRef.value) {
Expand Down
71 changes: 71 additions & 0 deletions tests/use-swrv.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,24 @@ describe('useSWRV', () => {
expect(wrapper.text()).toBe('hello, SWR')
})

it('should update refs when mutating a computed array key', async () => {
const id = ref('1')
const wrapper = mount(defineComponent({
template: '<div>{{ data }}</div>',
setup () {
const computedKey = computed(() => ['computed-array-key', id.value])
return useSWRV(computedKey, null)
}
}))

expect(wrapper.text()).toBe('')

await mutate(['computed-array-key', '1'], 'SWR')
await tick()

expect(wrapper.text()).toBe('SWR')
})

it('should accept object args', async () => {
const obj = { v: 'hello' }
const arr = ['world']
Expand Down Expand Up @@ -800,6 +818,29 @@ describe('useSWRV - loading', () => {
expect(wrapper.text()).toBe(':ready')
})

it('should clear loading state when key becomes nullish during revalidation', async () => {
const key = ref('loading-key')
const wrapper = mount(defineComponent({
template: '<div>isValidating: {{ isValidating }}, isLoading: {{ isLoading }}</div>',
setup () {
const { isValidating, isLoading } = useSWRV(() => key.value, () => new Promise(res => setTimeout(() => res('SWR'), 200)))
return { isValidating, isLoading }
}
}))

expect(wrapper.text()).toBe('isValidating: true, isLoading: true')

key.value = null
await tick(2)

expect(wrapper.text()).toBe('isValidating: false, isLoading: false')

timeout(200)
await tick(2)

expect(wrapper.text()).toBe('isValidating: false, isLoading: false')
})

it('should indicate cached data from another key with isLoading false', async () => {
const key = ref(1)
const wrapper = mount(defineComponent({
Expand Down Expand Up @@ -870,6 +911,36 @@ describe('useSWRV - loading', () => {
// data loaded
expect(wrapper.text()).toBe('data: data-3, isValidating: false, isLoading: false')
})

it('should keep validating when an old key resolves after the current key changes', async () => {
const key = ref('old')
const wrapper = mount(defineComponent({
template: '<div>data: {{ data }}, isValidating: {{ isValidating }}</div>',
setup () {
const { data, isValidating } = useSWRV(() => key.value, currentKey => {
const delay = currentKey === 'old' ? 200 : 400
return new Promise(res => setTimeout(() => res(currentKey), delay))
})

return { data, isValidating }
}
}))

expect(wrapper.text()).toBe('data: , isValidating: true')

timeout(100)
key.value = 'new'
await tick(2)
expect(wrapper.text()).toBe('data: , isValidating: true')

timeout(100)
await tick(2)
expect(wrapper.text()).toBe('data: , isValidating: true')

timeout(300)
await tick(2)
expect(wrapper.text()).toBe('data: new, isValidating: false')
})
})

describe('useSWRV - mutate', () => {
Expand Down