diff --git a/src/index.ts b/src/index.ts index f46a99e97..47a9f2f4f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ export { BareFetcher, Fetcher, MutatorCallback, + MutatorOptions, Middleware, Arguments } from './types' diff --git a/src/types.ts b/src/types.ts index 70ededc14..f1ab3e6ba 100644 --- a/src/types.ts +++ b/src/types.ts @@ -142,13 +142,19 @@ export type MutatorCallback = ( currentValue?: Data ) => Promise | undefined | Data +export type MutatorOptions = { + revalidate?: boolean + populateCache?: boolean +} + export type Broadcaster = ( cache: Cache, key: string, data: Data, error?: Error, isValidating?: boolean, - shouldRevalidate?: boolean + revalidate?: boolean, + populateCache?: boolean ) => Promise export type State = { @@ -161,7 +167,7 @@ export type Mutator = ( cache: Cache, key: Key, data?: Data | Promise | MutatorCallback, - shouldRevalidate?: boolean + opts?: boolean | MutatorOptions ) => Promise export interface ScopedMutator { @@ -169,19 +175,19 @@ export interface ScopedMutator { ( key: Key, data?: Data | Promise | MutatorCallback, - shouldRevalidate?: boolean + opts?: boolean | MutatorOptions ): Promise /** This is used for global mutator */ ( key: Key, data?: T | Promise | MutatorCallback, - shouldRevalidate?: boolean + opts?: boolean | MutatorOptions ): Promise } export type KeyedMutator = ( data?: Data | Promise | MutatorCallback, - shouldRevalidate?: boolean + opts?: boolean | MutatorOptions ) => Promise // Public types diff --git a/src/utils/broadcast-state.ts b/src/utils/broadcast-state.ts index 1d039b215..00fba6407 100644 --- a/src/utils/broadcast-state.ts +++ b/src/utils/broadcast-state.ts @@ -8,7 +8,8 @@ export const broadcastState: Broadcaster = ( data, error, isValidating, - revalidate + revalidate, + populateCache = true ) => { const [ EVENT_REVALIDATORS, @@ -21,9 +22,11 @@ export const broadcastState: Broadcaster = ( const revalidators = EVENT_REVALIDATORS[key] const updaters = STATE_UPDATERS[key] || [] - // Always update states of all hooks. - for (let i = 0; i < updaters.length; ++i) { - updaters[i](data, error, isValidating) + // Cache was populated, update states of all hooks. + if (populateCache && updaters) { + for (let i = 0; i < updaters.length; ++i) { + updaters[i](data, error, isValidating) + } } // If we also need to revalidate, only do it for the first hook. diff --git a/src/utils/mutate.ts b/src/utils/mutate.ts index 06913906d..f55d9834f 100644 --- a/src/utils/mutate.ts +++ b/src/utils/mutate.ts @@ -4,20 +4,26 @@ import { SWRGlobalState, GlobalState } from './global-state' import { broadcastState } from './broadcast-state' import { getTimestamp } from './timestamp' -import { Key, Cache, MutatorCallback } from '../types' +import { Key, Cache, MutatorCallback, MutatorOptions } from '../types' export const internalMutate = async ( ...args: [ Cache, Key, undefined | Data | Promise | MutatorCallback, - undefined | boolean + undefined | boolean | MutatorOptions ] ) => { - const [cache, _key] = args + const [cache, _key, _data, _opts] = args + + // When passing as a boolean, it's explicitily used to disable/enable + // revalidation. + const options = + typeof _opts === 'boolean' ? { revalidate: _opts } : _opts || {} + // Fallback to `true` if it's not explicitly set to `false` - const revalidate = args[3] !== false - let _data = args[2] + const revalidate = options.revalidate !== false + const populateCache = options.populateCache !== false // Serilaize key const [key, , keyErr] = serialize(_key) @@ -36,31 +42,33 @@ export const internalMutate = async ( cache.get(key), cache.get(keyErr), UNDEFINED, - revalidate + revalidate, + populateCache ) } - let data: any, error: unknown + let data: any = _data + let error: unknown // Update global timestamps. const beforeMutationTs = (MUTATION_TS[key] = getTimestamp()) MUTATION_END_TS[key] = 0 - if (isFunction(_data)) { - // `_data` is a function, call it passing current cache value. + 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)(cache.get(key)) } catch (err) { // If it throws an error synchronously, we shouldn't update the cache. error = err } } - // `_data` is a promise/thenable, resolve the final data first. - if (_data && isFunction((_data as Promise).then)) { + // `data` is a promise/thenable, resolve the final data first. + if (data && isFunction((data as Promise).then)) { // This means that the mutation is async, we need to check timestamps to // avoid race conditions. - data = await (_data as Promise).catch(err => { + data = await (data as Promise).catch(err => { error = err }) @@ -71,16 +79,16 @@ export const internalMutate = async ( if (error) throw error return data } - } else { - data = _data } - // Only update cached data if there's no error. Data can be `undefined` here. - if (!error) { - cache.set(key, data) + if (populateCache) { + if (!error) { + // Only update cached data if there's no error. Data can be `undefined` here. + cache.set(key, data) + } + // Always update or reset the error. + cache.set(keyErr, error) } - // Always update or reset the error. - cache.set(keyErr, error) // Reset the timestamp to mark the mutation has ended. MUTATION_END_TS[key] = getTimestamp() @@ -92,10 +100,11 @@ export const internalMutate = async ( data, error, UNDEFINED, - revalidate + revalidate, + populateCache ) // Throw error or return data if (error) throw error - return res + return populateCache ? res : data } diff --git a/test/use-swr-local-mutation.test.tsx b/test/use-swr-local-mutation.test.tsx index 959751f32..04e9e9d48 100644 --- a/test/use-swr-local-mutation.test.tsx +++ b/test/use-swr-local-mutation.test.tsx @@ -982,4 +982,33 @@ describe('useSWR - local mutation', () => { await sleep(300) await screen.findByText('success') }) + + it('should not update the cache when `populateCache` is disabled', async () => { + const key = createKey() + function Page() { + const { data, mutate } = useSWR(key, () => 'foo') + return ( + <> +
data: {String(data)}
+ + + ) + } + + renderWithConfig() + await screen.findByText('data: foo') + + fireEvent.click(screen.getByText('mutate')) + await sleep(30) + await screen.findByText('data: foo') + }) })