From 81d8052ba8a59f7e7c7f53cab8c8d2146e68c51c Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Fri, 18 Feb 2022 22:02:31 +0100 Subject: [PATCH 1/7] store all data fields using one single key --- .../revalidate-events.ts => constants.ts} | 0 src/types.ts | 2 +- src/use-swr.ts | 47 ++++++++++--------- src/utils/broadcast-state.ts | 2 +- src/utils/cache.ts | 2 +- src/utils/mutate.ts | 18 +++++-- 6 files changed, 41 insertions(+), 30 deletions(-) rename src/{constants/revalidate-events.ts => constants.ts} (100%) diff --git a/src/constants/revalidate-events.ts b/src/constants.ts similarity index 100% rename from src/constants/revalidate-events.ts rename to src/constants.ts diff --git a/src/types.ts b/src/types.ts index 541267709..4debbc055 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import * as revalidateEvents from './constants/revalidate-events' +import * as revalidateEvents from './constants' export type FetcherResponse = Data | Promise export type BareFetcher = ( diff --git a/src/use-swr.ts b/src/use-swr.ts index 53573f00b..6aaa23c5a 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -17,7 +17,7 @@ import { subscribeCallback } from './utils/subscribe-key' import { broadcastState } from './utils/broadcast-state' import { getTimestamp } from './utils/timestamp' import { internalMutate } from './utils/mutate' -import * as revalidateEvents from './constants/revalidate-events' +import * as revalidateEvents from './constants' import { State, Fetcher, @@ -57,7 +57,7 @@ export const useSWRHandler = ( // all of them are derived from `_key`. // `fnArgs` is an array of arguments parsed from the key, which will be passed // to the fetcher. - const [key, fnArgs, keyInfo] = serialize(_key) + const [key, fnArgs] = serialize(_key) // If it's the initial render of this hook. const initialMountedRef = useRef(false) @@ -72,17 +72,24 @@ export const useSWRHandler = ( const configRef = useRef(config) const getConfig = () => configRef.current const isActive = () => getConfig().isVisible() && getConfig().isOnline() - const patchFetchInfo = (info: { isValidating?: boolean; error?: any }) => - cache.set(keyInfo, mergeObjects(cache.get(keyInfo), info)) + + const getCache = () => cache.get(key) || {} + const setCache = (info: { + data?: Data + error?: any + isValidating?: boolean + }) => { + cache.set(key, mergeObjects(cache.get(key), info)) + } // Get the current state that SWR should return. - const cached = cache.get(key) + const cached = getCache() + const cachedData = cached.data const fallback = isUndefined(fallbackData) ? config.fallback[key] : fallbackData - const data = isUndefined(cached) ? fallback : cached - const info = cache.get(keyInfo) || {} - const error = info.error + const data = isUndefined(cachedData) ? fallback : cachedData + const error = cached.error const isInitialMount = !initialMountedRef.current @@ -110,7 +117,7 @@ export const useSWRHandler = ( // Resolve the current validating state. const resolveValidating = () => { if (!key || !fetcher) return false - if (info.isValidating) return true + if (cached.isValidating) return true // If it's not mounted yet and it should revalidate on mount, revalidate. return isInitialMount && shouldRevalidate() @@ -170,7 +177,7 @@ export const useSWRHandler = ( // The new state object when request finishes. const newState: State = { isValidating: false } const finishRequestAndUpdateState = () => { - patchFetchInfo({ isValidating: false }) + setCache({ isValidating: false }) // We can only set state if it's safe (still mounted with the same key). if (isCurrentKeyMounted()) { setState(newState) @@ -178,9 +185,7 @@ export const useSWRHandler = ( } // Start fetching. Change the `isValidating` state, update the cache. - patchFetchInfo({ - isValidating: true - }) + setCache({ isValidating: true }) setState({ isValidating: true }) try { @@ -196,7 +201,7 @@ export const useSWRHandler = ( // If no cache being rendered currently (it shows a blank page), // we trigger the loading slow event. - if (config.loadingTimeout && !cache.get(key)) { + if (config.loadingTimeout && isUndefined(getCache().data)) { setTimeout(() => { if (loading && isCurrentKeyMounted()) { getConfig().onLoadingSlow(key, config) @@ -235,9 +240,7 @@ export const useSWRHandler = ( } // Clear error. - patchFetchInfo({ - error: UNDEFINED - }) + setCache({ error: UNDEFINED }) newState.error = UNDEFINED // If there're other mutations(s), overlapped with the current revalidation: @@ -285,8 +288,8 @@ export const useSWRHandler = ( // For global state, it's possible that the key has changed. // https://github.com/vercel/swr/pull/1058 - if (!compare(cache.get(key), newData)) { - cache.set(key, newData) + if (!compare(getCache().data, newData)) { + setCache({ data: newData }) } // Trigger the successful callback if it's the original request. @@ -301,7 +304,7 @@ export const useSWRHandler = ( // Not paused, we continue handling the error. Otherwise discard it. if (!getConfig().isPaused()) { // Get a new error, don't use deep comparison for errors. - patchFetchInfo({ error: err }) + setCache({ error: err }) newState.error = err as Error // Error event and retry logic. Only for the actual request, not @@ -342,8 +345,8 @@ export const useSWRHandler = ( return true }, - // `setState` is immutable, and `eventsCallback`, `fnArgs`, `keyInfo`, - // and `keyValidating` are depending on `key`, so we can exclude them from + // `setState` is immutable, and `eventsCallback`, `fnArgs`, and + // `keyValidating` are depending on `key`, so we can exclude them from // the deps array. // // FIXME: diff --git a/src/utils/broadcast-state.ts b/src/utils/broadcast-state.ts index eccf8b141..1c5cc1c6f 100644 --- a/src/utils/broadcast-state.ts +++ b/src/utils/broadcast-state.ts @@ -1,6 +1,6 @@ import { Broadcaster } from '../types' import { SWRGlobalState, GlobalState } from './global-state' -import * as revalidateEvents from '../constants/revalidate-events' +import * as revalidateEvents from '../constants' export const broadcastState: Broadcaster = ( cache, diff --git a/src/utils/cache.ts b/src/utils/cache.ts index 7ff30f4a8..c2af5cd59 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -3,7 +3,7 @@ import { IS_SERVER } from './env' import { UNDEFINED, mergeObjects, noop } from './helper' import { internalMutate } from './mutate' import { GlobalState, SWRGlobalState } from './global-state' -import * as revalidateEvents from '../constants/revalidate-events' +import * as revalidateEvents from '../constants' import { RevalidateEvent } from '../types' import { diff --git a/src/utils/mutate.ts b/src/utils/mutate.ts index a42b27bed..576e54062 100644 --- a/src/utils/mutate.ts +++ b/src/utils/mutate.ts @@ -30,9 +30,17 @@ export const internalMutate = async ( const customOptimisticData = options.optimisticData // Serilaize key - const [key, , keyInfo] = serialize(_key) + const [key] = serialize(_key) if (!key) return + const setCache = (info: { + data?: Data + error?: any + isValidating?: boolean + }) => { + cache.set(key, mergeObjects(cache.get(key), info)) + } + const [, , MUTATION] = SWRGlobalState.get(cache) as GlobalState // If there is no new data provided, revalidate the key with current state. @@ -63,7 +71,7 @@ export const internalMutate = async ( const optimisticData = isFunction(customOptimisticData) ? customOptimisticData(rollbackData) : customOptimisticData - cache.set(key, optimisticData) + setCache({ data: optimisticData }) broadcastState(cache, key, optimisticData) } @@ -96,7 +104,7 @@ export const internalMutate = async ( // transforming the data. populateCache = true data = rollbackData - cache.set(key, rollbackData) + setCache({ data: rollbackData }) } } @@ -109,11 +117,11 @@ export const internalMutate = async ( } // Only update cached data if there's no error. Data can be `undefined` here. - cache.set(key, data) + setCache({ data }) } // Always update or reset the error. - cache.set(keyInfo, mergeObjects(cache.get(keyInfo), { error })) + setCache({ error }) } // Reset the timestamp to mark the mutation has ended. From f772e18077c36908ca1f8085ab653f1b2094eb9a Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sat, 19 Feb 2022 23:44:08 +0100 Subject: [PATCH 2/7] fix tests --- src/types.ts | 8 ++--- src/use-swr.ts | 27 +++++++---------- src/utils/broadcast-state.ts | 12 ++++---- src/utils/mutate.ts | 44 ++++++++++++---------------- test/use-swr-local-mutation.test.tsx | 10 +++---- 5 files changed, 43 insertions(+), 58 deletions(-) diff --git a/src/types.ts b/src/types.ts index 4debbc055..e92420700 100644 --- a/src/types.ts +++ b/src/types.ts @@ -248,11 +248,11 @@ export type RevalidateCallback = ( type: K ) => RevalidateCallbackReturnType[K] -export type StateUpdateCallback = ( - data?: Data, - error?: Error, +export type StateUpdateCallback = (state: { + data?: Data + error?: Error isValidating?: boolean -) => void +}) => void export interface Cache { get(key: Key): Data | null | undefined diff --git a/src/use-swr.ts b/src/use-swr.ts index 6aaa23c5a..453777b49 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -191,13 +191,10 @@ export const useSWRHandler = ( try { if (shouldStartNewRequest) { // Tell all other hooks to change the `isValidating` state. - broadcastState( - cache, - key, - stateRef.current.data, - stateRef.current.error, - true - ) + broadcastState(cache, key, { + ...stateRef.current, + isValidating: true + }) // If no cache being rendered currently (it shows a blank page), // we trigger the loading slow event. @@ -340,7 +337,7 @@ export const useSWRHandler = ( // Here is the source of the request, need to tell all other hooks to // update their states. if (isCurrentKeyMounted() && shouldStartNewRequest) { - broadcastState(cache, key, newState.data, newState.error, false) + broadcastState(cache, key, { ...newState, isValidating: false }) } return true @@ -386,23 +383,19 @@ export const useSWRHandler = ( // Expose state updater to global event listeners. So we can update hook's // internal state from the outside. - const onStateUpdate: StateUpdateCallback = ( - updatedData, - updatedError, - updatedIsValidating - ) => { + const onStateUpdate: StateUpdateCallback = state => { setState( mergeObjects( { - error: updatedError, - isValidating: updatedIsValidating + error: state.error, + isValidating: state.isValidating }, // Since `setState` only shallowly compares states, we do a deep // comparison here. - compare(stateRef.current.data, updatedData) + compare(stateRef.current.data, state.data) ? UNDEFINED : { - data: updatedData + data: state.data } ) ) diff --git a/src/utils/broadcast-state.ts b/src/utils/broadcast-state.ts index 1c5cc1c6f..b02c0f6b9 100644 --- a/src/utils/broadcast-state.ts +++ b/src/utils/broadcast-state.ts @@ -5,9 +5,7 @@ import * as revalidateEvents from '../constants' export const broadcastState: Broadcaster = ( cache, key, - data, - error, - isValidating, + state, revalidate, broadcast = true ) => { @@ -20,7 +18,7 @@ export const broadcastState: Broadcaster = ( // Cache was populated, update states of all hooks. if (broadcast && updaters) { for (let i = 0; i < updaters.length; ++i) { - updaters[i](data, error, isValidating) + updaters[i](state) } } @@ -31,11 +29,11 @@ export const broadcastState: Broadcaster = ( delete FETCH[key] if (revalidators && revalidators[0]) { - return revalidators[0](revalidateEvents.MUTATE_EVENT).then(() => - cache.get(key) + return revalidators[0](revalidateEvents.MUTATE_EVENT).then( + () => cache.get(key).data ) } } - return cache.get(key) + return cache.get(key).data } diff --git a/src/utils/mutate.ts b/src/utils/mutate.ts index 576e54062..7cf448df0 100644 --- a/src/utils/mutate.ts +++ b/src/utils/mutate.ts @@ -25,9 +25,9 @@ export const internalMutate = async ( let populateCache = isUndefined(options.populateCache) ? true : options.populateCache + let optimisticData = options.optimisticData const revalidate = options.revalidate !== false const rollbackOnError = options.rollbackOnError !== false - const customOptimisticData = options.optimisticData // Serilaize key const [key] = serialize(_key) @@ -46,15 +46,7 @@ export const internalMutate = async ( // If there is no new data provided, revalidate the key with current state. if (args.length < 3) { // Revalidate and broadcast state. - return broadcastState( - cache, - key, - cache.get(key), - UNDEFINED, - UNDEFINED, - revalidate, - true - ) + return broadcastState(cache, key, cache.get(key), revalidate, true) } let data: any = _data @@ -63,22 +55,23 @@ export const internalMutate = async ( // Update global timestamps. const beforeMutationTs = getTimestamp() MUTATION[key] = [beforeMutationTs, 0] - const hasCustomOptimisticData = !isUndefined(customOptimisticData) - const rollbackData = cache.get(key) + + const hasOptimisticData = !isUndefined(optimisticData) + const originalData = cache.get(key)?.data // Do optimistic data update. - if (hasCustomOptimisticData) { - const optimisticData = isFunction(customOptimisticData) - ? customOptimisticData(rollbackData) - : customOptimisticData + if (hasOptimisticData) { + optimisticData = isFunction(optimisticData) + ? optimisticData(originalData) + : optimisticData setCache({ data: optimisticData }) - broadcastState(cache, key, optimisticData) + broadcastState(cache, key, { data: optimisticData }) } if (isFunction(data)) { // `data` is a function, call it passing current cache value. try { - data = (data as MutatorCallback)(cache.get(key)) + data = (data as MutatorCallback)(originalData) } catch (err) { // If it throws an error synchronously, we shouldn't update the cache. error = err @@ -99,12 +92,12 @@ export const internalMutate = async ( if (beforeMutationTs !== MUTATION[key][0]) { if (error) throw error return data - } else if (error && hasCustomOptimisticData && rollbackOnError) { + } else if (error && hasOptimisticData && rollbackOnError) { // Rollback. Always populate the cache in this case but without // transforming the data. populateCache = true - data = rollbackData - setCache({ data: rollbackData }) + data = originalData + setCache({ data: originalData }) } } @@ -113,7 +106,7 @@ export const internalMutate = async ( if (!error) { // Transform the result into data. if (isFunction(populateCache)) { - data = populateCache(data, rollbackData) + data = populateCache(data, originalData) } // Only update cached data if there's no error. Data can be `undefined` here. @@ -131,9 +124,10 @@ export const internalMutate = async ( const res = await broadcastState( cache, key, - data, - error, - UNDEFINED, + { + data, + error + }, revalidate, !!populateCache ) diff --git a/test/use-swr-local-mutation.test.tsx b/test/use-swr-local-mutation.test.tsx index 074e6427f..2699a67fe 100644 --- a/test/use-swr-local-mutation.test.tsx +++ b/test/use-swr-local-mutation.test.tsx @@ -272,7 +272,7 @@ describe('useSWR - local mutation', () => { // Prefill the cache with data renderWithConfig(, { - provider: () => new Map([[key, 'cached data']]) + provider: () => new Map([[key, { data: 'cached data' }]]) }) const callback = jest.fn() @@ -303,7 +303,7 @@ describe('useSWR - local mutation', () => { expect(increment).toHaveBeenLastCalledWith(undefined) expect(increment).toHaveLastReturnedWith(undefined) - cache.set(key, 42) + cache.set(key, { ...cache.get(key), data: 42 }) await mutate(key, increment, false) @@ -536,12 +536,12 @@ describe('useSWR - local mutation', () => { }) screen.getByText(message) - const [keyData, , keyInfo] = serialize(key) + const [keyInfo] = serialize(key) let cacheError = cache.get(keyInfo)?.error expect(cacheError.message).toMatchInlineSnapshot(`"${message}"`) // if mutate throws an error synchronously, the cache shouldn't be updated - expect(cache.get(keyData)).toBe(value) + expect(cache.get(keyInfo)?.data).toBe(value) // if mutate succeed, error should be cleared await act(() => mutate(key, value, false)) @@ -807,7 +807,7 @@ describe('useSWR - local mutation', () => { createResponse('data', { delay: 30 }) ) const { cache } = useSWRConfig() - const [, , keyInfo] = serialize(key) + const [keyInfo] = serialize(key) const cacheIsValidating = cache.get(keyInfo)?.isValidating return ( <> From 85228b318caa4b4ed5d2d5a4b2e88978b2190b34 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sun, 20 Feb 2022 00:08:04 +0100 Subject: [PATCH 3/7] fix bugs and tests --- infinite/index.ts | 6 ++-- src/use-swr.ts | 2 +- test/use-swr-cache.test.tsx | 52 ++++++++++++++++++++-------------- test/use-swr-infinite.test.tsx | 8 ++++-- test/use-swr-refresh.test.tsx | 2 +- 5 files changed, 41 insertions(+), 29 deletions(-) diff --git a/infinite/index.ts b/infinite/index.ts index eac8d06c0..2fa859f46 100644 --- a/infinite/index.ts +++ b/infinite/index.ts @@ -128,7 +128,7 @@ export const infinite = ((useSWRNext: SWRHook) => } // Get the cached page data. - let pageData = cache.get(pageKey) + let pageData = cache.get(pageKey)?.data // should fetch (or revalidate) if: // - `revalidateAll` is enabled @@ -149,7 +149,7 @@ export const infinite = ((useSWRNext: SWRHook) => if (fn && shouldFetchPage) { pageData = await fn(...pageArgs) - cache.set(pageKey, pageData) + cache.set(pageKey, { ...cache.get(pageKey), data: pageData }) } data.push(pageData) @@ -216,7 +216,7 @@ export const infinite = ((useSWRNext: SWRHook) => const [pageKey] = serialize(getKey(i, previousPageData)) // Get the cached page data. - const pageData = pageKey ? cache.get(pageKey) : UNDEFINED + const pageData = pageKey ? cache.get(pageKey)?.data : UNDEFINED // Return the current data if we can't get it from the cache. if (isUndefined(pageData)) return dataRef.current diff --git a/src/use-swr.ts b/src/use-swr.ts index 453777b49..ddfb37e65 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -383,7 +383,7 @@ export const useSWRHandler = ( // Expose state updater to global event listeners. So we can update hook's // internal state from the outside. - const onStateUpdate: StateUpdateCallback = state => { + const onStateUpdate: StateUpdateCallback = (state = {}) => { setState( mergeObjects( { diff --git a/test/use-swr-cache.test.tsx b/test/use-swr-cache.test.tsx index 405a47dfb..b3965eb40 100644 --- a/test/use-swr-cache.test.tsx +++ b/test/use-swr-cache.test.tsx @@ -32,12 +32,12 @@ describe('useSWR - cache provider', () => { renderWithConfig(, { provider: () => provider }) await screen.findByText(fetcher(keys[0])) - expect(provider.get(keys[1])).toBe(undefined) + expect(provider.get(keys[1])?.data).toBe(undefined) fireEvent.click(screen.getByText(fetcher(keys[0]))) await act(() => sleep(10)) - expect(provider.get(keys[0])).toBe(fetcher(keys[0])) - expect(provider.get(keys[1])).toBe(fetcher(keys[1])) + expect(provider.get(keys[0])?.data).toBe(fetcher(keys[0])) + expect(provider.get(keys[1])?.data).toBe(fetcher(keys[1])) }) it('should be able to read from the initial cache with updates', async () => { @@ -52,7 +52,7 @@ describe('useSWR - cache provider', () => { } renderWithConfig(, { - provider: () => new Map([[key, 'cached value']]) + provider: () => new Map([[key, { data: 'cached value' }]]) }) screen.getByText('cached value') await screen.findByText('updated value') @@ -71,7 +71,7 @@ describe('useSWR - cache provider', () => { } renderWithConfig(, { - provider: () => new Map([[key, 'cached value']]) + provider: () => new Map([[key, { data: 'cached value' }]]) }) screen.getByText('cached value') await act(() => mutate(key, 'mutated value', false)) @@ -91,14 +91,18 @@ describe('useSWR - cache provider', () => { return (
{data}: - new Map([[key, '2']]) }}> + new Map([[key, { data: '2' }]]) }} + >
) } - renderWithConfig(, { provider: () => new Map([[key, '1']]) }) + renderWithConfig(, { + provider: () => new Map([[key, { data: '1' }]]) + }) screen.getByText('1:2') }) @@ -113,11 +117,15 @@ describe('useSWR - cache provider', () => { function Page() { return (
- new Map([[key, '1']]) }}> + new Map([[key, { data: '1' }]]) }} + > : - new Map([[key, '2']]) }}> + new Map([[key, { data: '2' }]]) }} + >
@@ -142,7 +150,7 @@ describe('useSWR - cache provider', () => { return <>{String(data)} } const { unmount } = renderWithConfig(, { - provider: () => new Map([[key, 0]]), + provider: () => new Map([[key, { data: 0 }]]), initFocus() { focusFn() return unsubscribeFocusFn @@ -216,14 +224,14 @@ describe('useSWR - cache provider', () => { } renderWithConfig(, { - provider: () => new Map([[key, 'cache']]), + provider: () => new Map([[key, { data: 'cache' }]]), fallback: { [key]: 'fallback' } }) screen.getByText('cache') // no `undefined`, directly from cache await screen.findByText('data') }) - it('should be able to extend the parent cache', async () => { + it.skip('should be able to extend the parent cache', async () => { let parentCache const key = createKey() @@ -243,8 +251,10 @@ describe('useSWR - cache provider', () => { get: k => { // We append `-extended` to the value returned by the parent cache. const v = parentCache_.get(k) - if (typeof v === 'undefined') return v - return v + '-extended' + if (v && typeof v.data !== 'undefined') { + return { ...v, data: v.data + '-extended' } + } + return v }, delete: k => parentCache_.delete(k) } @@ -357,12 +367,12 @@ describe('useSWR - global cache', () => { renderWithGlobalCache() await screen.findByText(fetcher(keys[0])) - expect(cache.get(keys[1])).toBe(undefined) + expect(cache.get(keys[1])?.data).toBe(undefined) fireEvent.click(screen.getByText(fetcher(keys[0]))) await act(() => sleep(10)) - expect(cache.get(keys[0])).toBe(fetcher(keys[0])) - expect(cache.get(keys[1])).toBe(fetcher(keys[1])) + expect(cache.get(keys[0])?.data).toBe(fetcher(keys[0])) + expect(cache.get(keys[1])?.data).toBe(fetcher(keys[1])) }) it('should correctly mutate the cached value', async () => { @@ -419,7 +429,7 @@ describe('useSWR - global cache', () => { it('should reusing the same cache instance after unmounting SWRConfig', async () => { let focusEventRegistered = false - const cacheSingleton = new Map([['key', 'value']]) + const cacheSingleton = new Map([['key', { data: 'value' }]]) function Page() { return ( { } function Comp() { const { cache } = useSWRConfig() - return <>{String(cache.get('key'))} + return <>{String(cache.get('key')?.data)} } function Wrapper() { @@ -462,7 +472,7 @@ describe('useSWR - global cache', () => { it('should correctly return the cache instance under strict mode', async () => { function Page() { // Intentionally do this. - const [cache] = useState(new Map([['key', 'value']])) + const [cache] = useState(new Map([['key', { data: 'value' }]])) return ( cache }}> @@ -471,7 +481,7 @@ describe('useSWR - global cache', () => { } function Comp() { const { cache } = useSWRConfig() - return <>{String(cache.get('key'))} + return <>{String(cache.get('key')?.data)} } renderWithGlobalCache( diff --git a/test/use-swr-infinite.test.tsx b/test/use-swr-infinite.test.tsx index 9cbbc57d2..1537e0aca 100644 --- a/test/use-swr-infinite.test.tsx +++ b/test/use-swr-infinite.test.tsx @@ -776,7 +776,9 @@ describe('useSWRInfinite', () => { function App() { return ( new Map([[key, 'initial-cache']]) }} + value={{ + provider: () => new Map([[key, { data: 'initial-cache' }]]) + }} > @@ -950,7 +952,7 @@ describe('useSWRInfinite', () => { } renderWithConfig(, { - provider: () => new Map([[key + '-1', 'cached value']]) + provider: () => new Map([[key + '-1', { data: 'cached value' }]]) }) screen.getByText('data:') @@ -973,7 +975,7 @@ describe('useSWRInfinite', () => { ) } renderWithConfig(, { - provider: () => new Map([[key + '-1', 'cached value']]) + provider: () => new Map([[key + '-1', { data: 'cached value' }]]) }) screen.getByText('data:') diff --git a/test/use-swr-refresh.test.tsx b/test/use-swr-refresh.test.tsx index 05d9996ac..d09bb6c8d 100644 --- a/test/use-swr-refresh.test.tsx +++ b/test/use-swr-refresh.test.tsx @@ -259,7 +259,7 @@ describe('useSWR - refresh', () => { version: '1.0' }) - const cachedData = customCache.get(key) + const cachedData = customCache.get(key)?.data expect(cachedData.timestamp.toString()).toEqual('1') screen.getByText('1') }) From 0b567937051c0b95b86251a9ae1ea45b1852dd53 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sun, 20 Feb 2022 00:08:44 +0100 Subject: [PATCH 4/7] fix lint error --- src/utils/mutate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/mutate.ts b/src/utils/mutate.ts index 7cf448df0..bb70fa883 100644 --- a/src/utils/mutate.ts +++ b/src/utils/mutate.ts @@ -1,5 +1,5 @@ import { serialize } from './serialize' -import { isFunction, isUndefined, mergeObjects, UNDEFINED } from './helper' +import { isFunction, isUndefined, mergeObjects } from './helper' import { SWRGlobalState, GlobalState } from './global-state' import { broadcastState } from './broadcast-state' import { getTimestamp } from './timestamp' From 0259fd16b15f40cda1b34fad9df303a57802faea Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sun, 3 Apr 2022 17:55:43 +0200 Subject: [PATCH 5/7] refactor code --- src/use-swr.ts | 11 +++-------- src/utils/cache.ts | 13 ++++++++++++- src/utils/mutate.ts | 15 +++++---------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/use-swr.ts b/src/use-swr.ts index 6f62550f4..6ff916360 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -18,6 +18,8 @@ import { broadcastState } from './utils/broadcast-state' import { getTimestamp } from './utils/timestamp' import { internalMutate } from './utils/mutate' import * as revalidateEvents from './constants' +import { createCacheHelper } from './utils/cache' + import { State, Fetcher, @@ -85,14 +87,7 @@ export const useSWRHandler = ( const getConfig = () => configRef.current const isActive = () => getConfig().isVisible() && getConfig().isOnline() - const getCache = () => cache.get(key) || {} - const setCache = (info: { - data?: Data - error?: any - isValidating?: boolean - }) => { - cache.set(key, mergeObjects(cache.get(key), info)) - } + const [getCache, setCache] = createCacheHelper(cache, key) // Get the current state that SWR should return. const cached = getCache() diff --git a/src/utils/cache.ts b/src/utils/cache.ts index c2af5cd59..d199a7b4a 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -4,11 +4,12 @@ import { UNDEFINED, mergeObjects, noop } from './helper' import { internalMutate } from './mutate' import { GlobalState, SWRGlobalState } from './global-state' import * as revalidateEvents from '../constants' -import { RevalidateEvent } from '../types' import { + Key, Cache, ScopedMutator, + RevalidateEvent, RevalidateCallback, ProviderConfiguration } from '../types' @@ -98,3 +99,13 @@ export const initCache = ( return [provider, (SWRGlobalState.get(provider) as GlobalState)[4]] } + +export const createCacheHelper = (cache: Cache, key: Key) => + [ + // Getter + () => cache.get(key) || {}, + // Setter + (info: { data?: Data; error?: any; isValidating?: boolean }) => { + cache.set(key, mergeObjects(cache.get(key), info)) + } + ] as const diff --git a/src/utils/mutate.ts b/src/utils/mutate.ts index bb70fa883..5cb47f489 100644 --- a/src/utils/mutate.ts +++ b/src/utils/mutate.ts @@ -1,8 +1,9 @@ import { serialize } from './serialize' -import { isFunction, isUndefined, mergeObjects } from './helper' +import { isFunction, isUndefined } from './helper' import { SWRGlobalState, GlobalState } from './global-state' import { broadcastState } from './broadcast-state' import { getTimestamp } from './timestamp' +import { createCacheHelper } from './cache' import { Key, Cache, MutatorCallback, MutatorOptions } from '../types' @@ -33,20 +34,14 @@ export const internalMutate = async ( const [key] = serialize(_key) if (!key) return - const setCache = (info: { - data?: Data - error?: any - isValidating?: boolean - }) => { - cache.set(key, mergeObjects(cache.get(key), info)) - } + const [getCache, setCache] = createCacheHelper(cache, key) const [, , MUTATION] = SWRGlobalState.get(cache) as GlobalState // If there is no new data provided, revalidate the key with current state. if (args.length < 3) { // Revalidate and broadcast state. - return broadcastState(cache, key, cache.get(key), revalidate, true) + return broadcastState(cache, key, getCache(), revalidate, true) } let data: any = _data @@ -57,7 +52,7 @@ export const internalMutate = async ( MUTATION[key] = [beforeMutationTs, 0] const hasOptimisticData = !isUndefined(optimisticData) - const originalData = cache.get(key)?.data + const originalData = getCache().data // Do optimistic data update. if (hasOptimisticData) { From cfa545a91ee1dda3ac252d16d9bab6addf396a04 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Mon, 4 Apr 2022 11:16:46 +0200 Subject: [PATCH 6/7] get rid of $ and $ --- infinite/index.ts | 58 ++++++++++++++++++++++----------------------- src/use-swr.ts | 18 +++++++------- src/utils/cache.ts | 11 +++++++-- src/utils/mutate.ts | 14 +++++------ 4 files changed, 53 insertions(+), 48 deletions(-) diff --git a/infinite/index.ts b/infinite/index.ts index 3da71169d..9c1ad82f7 100644 --- a/infinite/index.ts +++ b/infinite/index.ts @@ -9,10 +9,12 @@ import useSWR, { Middleware, BareFetcher } from 'swr' + import { useIsomorphicLayoutEffect } from '../src/utils/env' import { serialize } from '../src/utils/serialize' import { isUndefined, isFunction, UNDEFINED } from '../src/utils/helper' import { withMiddleware } from '../src/utils/with-middleware' +import { createCacheHelper } from '../src/utils/cache' import type { SWRInfiniteConfiguration, @@ -52,34 +54,34 @@ export const infinite = ((useSWRNext: SWRHook) => revalidateOnMount = false } = config - // The serialized key of the first page. + // The serialized key of the first page. This key will be used to store + // metadata of this SWR infinite hook. let firstPageKey: string | null = null try { firstPageKey = getFirstPageKey(getKey) } catch (err) { - // not ready + // Not ready yet. } - // We use cache to pass extra info (context) to fetcher so it can be globally - // shared. The key of the context data is based on the first page key. - let contextCacheKey: string | null = null - - // Page size is also cached to share the page data between hooks with the - // same key. - let pageSizeCacheKey: string | null = null - - if (firstPageKey) { - contextCacheKey = '$ctx$' + firstPageKey - pageSizeCacheKey = '$len$' + firstPageKey - } + const [get, set] = createCacheHelper< + Data, + { + // We use cache to pass extra info (context) to fetcher so it can be globally + // shared. The key of the context data is based on the first page key. + $ctx: [boolean] | [boolean, Data[] | undefined] + // Page size is also cached to share the page data between hooks with the + // same key. + $len: number + } + >(cache, firstPageKey) const resolvePageSize = useCallback((): number => { - const cachedPageSize = cache.get(pageSizeCacheKey) + const cachedPageSize = get().$len return isUndefined(cachedPageSize) ? initialSize : cachedPageSize // `cache` isn't allowed to change during the lifecycle // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pageSizeCacheKey, initialSize]) + }, [firstPageKey, initialSize]) // keep the last page size to restore it with the persistSize option const lastPageSizeRef = useRef(resolvePageSize()) @@ -92,10 +94,7 @@ export const infinite = ((useSWRNext: SWRHook) => if (firstPageKey) { // If the key has been changed, we keep the current page size if persistSize is enabled - cache.set( - pageSizeCacheKey, - persistSize ? lastPageSizeRef.current : initialSize - ) + set({ $len: persistSize ? lastPageSizeRef.current : initialSize }) } // `initialSize` isn't allowed to change during the lifecycle @@ -110,8 +109,7 @@ export const infinite = ((useSWRNext: SWRHook) => firstPageKey ? INFINITE_PREFIX + firstPageKey : null, async () => { // get the revalidate context - const [forceRevalidateAll, originalData] = - cache.get(contextCacheKey) || [] + const [forceRevalidateAll, originalData] = get().$ctx || [] // return an array of page data const data: Data[] = [] @@ -157,7 +155,7 @@ export const infinite = ((useSWRNext: SWRHook) => } // once we executed the data fetching based on the context, clear the context - cache.delete(contextCacheKey) + set({ $ctx: UNDEFINED }) // return the data return data @@ -186,16 +184,16 @@ export const infinite = ((useSWRNext: SWRHook) => const shouldRevalidate = args[1] !== false // It is possible that the key is still falsy. - if (!contextCacheKey) return + if (!firstPageKey) return if (shouldRevalidate) { if (!isUndefined(data)) { // We only revalidate the pages that are changed const originalData = dataRef.current - cache.set(contextCacheKey, [false, originalData]) + set({ $ctx: [false, originalData] }) } else { // Calling `mutate()`, we revalidate all pages - cache.set(contextCacheKey, [true]) + set({ $ctx: [true] }) } } @@ -203,7 +201,7 @@ export const infinite = ((useSWRNext: SWRHook) => }, // swr.mutate is always the same reference // eslint-disable-next-line react-hooks/exhaustive-deps - [contextCacheKey] + [firstPageKey] ) // Function to load pages data from the cache based on the page size. @@ -233,7 +231,7 @@ export const infinite = ((useSWRNext: SWRHook) => const setSize = useCallback( (arg: number | ((size: number) => number)) => { // It is possible that the key is still falsy. - if (!pageSizeCacheKey) return + if (!firstPageKey) return let size if (isFunction(arg)) { @@ -243,14 +241,14 @@ export const infinite = ((useSWRNext: SWRHook) => } if (typeof size != 'number') return - cache.set(pageSizeCacheKey, size) + set({ $len: size }) lastPageSizeRef.current = size rerender({}) return mutate(resolvePagesFromCache(size)) }, // `cache` and `rerender` isn't allowed to change during the lifecycle // eslint-disable-next-line react-hooks/exhaustive-deps - [pageSizeCacheKey, resolvePageSize, mutate] + [firstPageKey, resolvePageSize, mutate] ) // Use getter functions to avoid unnecessary re-renders caused by triggering diff --git a/src/use-swr.ts b/src/use-swr.ts index 6ff916360..045f97987 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -87,10 +87,10 @@ export const useSWRHandler = ( const getConfig = () => configRef.current const isActive = () => getConfig().isVisible() && getConfig().isOnline() - const [getCache, setCache] = createCacheHelper(cache, key) + const [get, set] = createCacheHelper(cache, key) // Get the current state that SWR should return. - const cached = getCache() + const cached = get() const cachedData = cached.data const fallback = isUndefined(fallbackData) ? config.fallback[key] @@ -184,7 +184,7 @@ export const useSWRHandler = ( // The new state object when request finishes. const newState: State = { isValidating: false } const finishRequestAndUpdateState = () => { - setCache({ isValidating: false }) + set({ isValidating: false }) // We can only set state if it's safe (still mounted with the same key). if (isCurrentKeyMounted()) { setState(newState) @@ -192,7 +192,7 @@ export const useSWRHandler = ( } // Start fetching. Change the `isValidating` state, update the cache. - setCache({ isValidating: true }) + set({ isValidating: true }) setState({ isValidating: true }) try { @@ -205,7 +205,7 @@ export const useSWRHandler = ( // If no cache being rendered currently (it shows a blank page), // we trigger the loading slow event. - if (config.loadingTimeout && isUndefined(getCache().data)) { + if (config.loadingTimeout && isUndefined(get().data)) { setTimeout(() => { if (loading && isCurrentKeyMounted()) { getConfig().onLoadingSlow(key, config) @@ -248,7 +248,7 @@ export const useSWRHandler = ( } // Clear error. - setCache({ error: UNDEFINED }) + set({ error: UNDEFINED }) newState.error = UNDEFINED // If there're other mutations(s), overlapped with the current revalidation: @@ -296,8 +296,8 @@ export const useSWRHandler = ( // For global state, it's possible that the key has changed. // https://github.com/vercel/swr/pull/1058 - if (!compare(getCache().data, newData)) { - setCache({ data: newData }) + if (!compare(get().data, newData)) { + set({ data: newData }) } // Trigger the successful callback if it's the original request. @@ -312,7 +312,7 @@ export const useSWRHandler = ( // Not paused, we continue handling the error. Otherwise discard it. if (!getConfig().isPaused()) { // Get a new error, don't use deep comparison for errors. - setCache({ error: err }) + set({ error: err }) newState.error = err as Error // Error event and retry logic. Only for the actual request, not diff --git a/src/utils/cache.ts b/src/utils/cache.ts index d199a7b4a..b45ab5e5c 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -100,12 +100,19 @@ export const initCache = ( return [provider, (SWRGlobalState.get(provider) as GlobalState)[4]] } -export const createCacheHelper = (cache: Cache, key: Key) => +export const createCacheHelper = ( + cache: Cache, + key: Key +) => [ // Getter () => cache.get(key) || {}, // Setter - (info: { data?: Data; error?: any; isValidating?: boolean }) => { + ( + info: Partial< + { data: Data; error: any; isValidating: boolean } | ExtendedInfo + > + ) => { cache.set(key, mergeObjects(cache.get(key), info)) } ] as const diff --git a/src/utils/mutate.ts b/src/utils/mutate.ts index 5cb47f489..9b224ea2c 100644 --- a/src/utils/mutate.ts +++ b/src/utils/mutate.ts @@ -34,14 +34,14 @@ export const internalMutate = async ( const [key] = serialize(_key) if (!key) return - const [getCache, setCache] = createCacheHelper(cache, key) + const [get, set] = createCacheHelper(cache, key) const [, , MUTATION] = SWRGlobalState.get(cache) as GlobalState // If there is no new data provided, revalidate the key with current state. if (args.length < 3) { // Revalidate and broadcast state. - return broadcastState(cache, key, getCache(), revalidate, true) + return broadcastState(cache, key, get(), revalidate, true) } let data: any = _data @@ -52,14 +52,14 @@ export const internalMutate = async ( MUTATION[key] = [beforeMutationTs, 0] const hasOptimisticData = !isUndefined(optimisticData) - const originalData = getCache().data + const originalData = get().data // Do optimistic data update. if (hasOptimisticData) { optimisticData = isFunction(optimisticData) ? optimisticData(originalData) : optimisticData - setCache({ data: optimisticData }) + set({ data: optimisticData }) broadcastState(cache, key, { data: optimisticData }) } @@ -92,7 +92,7 @@ export const internalMutate = async ( // transforming the data. populateCache = true data = originalData - setCache({ data: originalData }) + set({ data: originalData }) } } @@ -105,11 +105,11 @@ export const internalMutate = async ( } // Only update cached data if there's no error. Data can be `undefined` here. - setCache({ data }) + set({ data }) } // Always update or reset the error. - setCache({ error }) + set({ error }) } // Reset the timestamp to mark the mutation has ended. From 73dc35b9597f7390636e08f302d15e120f9f6ba7 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Mon, 4 Apr 2022 11:49:55 +0200 Subject: [PATCH 7/7] simplify serialize --- infinite/index.ts | 23 ++++++++++++----------- src/utils/serialize.ts | 5 ++--- test/use-swr-infinite.test.tsx | 28 ++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/infinite/index.ts b/infinite/index.ts index 9c1ad82f7..56297fc95 100644 --- a/infinite/index.ts +++ b/infinite/index.ts @@ -56,9 +56,10 @@ export const infinite = ((useSWRNext: SWRHook) => // The serialized key of the first page. This key will be used to store // metadata of this SWR infinite hook. - let firstPageKey: string | null = null + let infiniteKey: string | undefined try { - firstPageKey = getFirstPageKey(getKey) + infiniteKey = getFirstPageKey(getKey) + if (infiniteKey) infiniteKey = INFINITE_PREFIX + infiniteKey } catch (err) { // Not ready yet. } @@ -73,7 +74,7 @@ export const infinite = ((useSWRNext: SWRHook) => // same key. $len: number } - >(cache, firstPageKey) + >(cache, infiniteKey) const resolvePageSize = useCallback((): number => { const cachedPageSize = get().$len @@ -81,7 +82,7 @@ export const infinite = ((useSWRNext: SWRHook) => // `cache` isn't allowed to change during the lifecycle // eslint-disable-next-line react-hooks/exhaustive-deps - }, [firstPageKey, initialSize]) + }, [infiniteKey, initialSize]) // keep the last page size to restore it with the persistSize option const lastPageSizeRef = useRef(resolvePageSize()) @@ -92,21 +93,21 @@ export const infinite = ((useSWRNext: SWRHook) => return } - if (firstPageKey) { + if (infiniteKey) { // If the key has been changed, we keep the current page size if persistSize is enabled set({ $len: persistSize ? lastPageSizeRef.current : initialSize }) } // `initialSize` isn't allowed to change during the lifecycle // eslint-disable-next-line react-hooks/exhaustive-deps - }, [firstPageKey]) + }, [infiniteKey]) // Needs to check didMountRef during mounting, not in the fetcher const shouldRevalidateOnMount = revalidateOnMount && !didMountRef.current // Actual SWR hook to load all pages in one fetcher. const swr = useSWRNext( - firstPageKey ? INFINITE_PREFIX + firstPageKey : null, + infiniteKey, async () => { // get the revalidate context const [forceRevalidateAll, originalData] = get().$ctx || [] @@ -184,7 +185,7 @@ export const infinite = ((useSWRNext: SWRHook) => const shouldRevalidate = args[1] !== false // It is possible that the key is still falsy. - if (!firstPageKey) return + if (!infiniteKey) return if (shouldRevalidate) { if (!isUndefined(data)) { @@ -201,7 +202,7 @@ export const infinite = ((useSWRNext: SWRHook) => }, // swr.mutate is always the same reference // eslint-disable-next-line react-hooks/exhaustive-deps - [firstPageKey] + [infiniteKey] ) // Function to load pages data from the cache based on the page size. @@ -231,7 +232,7 @@ export const infinite = ((useSWRNext: SWRHook) => const setSize = useCallback( (arg: number | ((size: number) => number)) => { // It is possible that the key is still falsy. - if (!firstPageKey) return + if (!infiniteKey) return let size if (isFunction(arg)) { @@ -248,7 +249,7 @@ export const infinite = ((useSWRNext: SWRHook) => }, // `cache` and `rerender` isn't allowed to change during the lifecycle // eslint-disable-next-line react-hooks/exhaustive-deps - [firstPageKey, resolvePageSize, mutate] + [infiniteKey, resolvePageSize, mutate] ) // Use getter functions to avoid unnecessary re-renders caused by triggering diff --git a/src/utils/serialize.ts b/src/utils/serialize.ts index b4233ac93..6eda57876 100644 --- a/src/utils/serialize.ts +++ b/src/utils/serialize.ts @@ -3,7 +3,7 @@ import { isFunction } from './helper' import { Key } from '../types' -export const serialize = (key: Key): [string, Key, string] => { +export const serialize = (key: Key): [string, Key] => { if (isFunction(key)) { try { key = key() @@ -25,6 +25,5 @@ export const serialize = (key: Key): [string, Key, string] => { ? stableHash(key) : '' - const infoKey = key ? '$swr$' + key : '' - return [key, args, infoKey] + return [key, args] } diff --git a/test/use-swr-infinite.test.tsx b/test/use-swr-infinite.test.tsx index b649ec51e..05bb3603b 100644 --- a/test/use-swr-infinite.test.tsx +++ b/test/use-swr-infinite.test.tsx @@ -693,6 +693,34 @@ describe('useSWRInfinite', () => { await screen.findByText('data:') }) + it('should support getKey to return null', async () => { + function Page() { + const { data, setSize } = useSWRInfinite( + () => null, + () => 'data' + ) + + return ( +
{ + // load next page + setSize(size => size + 1) + }} + > + data:{data || ''} +
+ ) + } + + renderWithConfig() + screen.getByText('data:') + await screen.findByText('data:') + + // load next page + fireEvent.click(screen.getByText('data:')) + await screen.findByText('data:') + }) + it('should mutate a cache with `unstable_serialize`', async () => { let count = 0 const key = createKey()