Skip to content

Commit

Permalink
feat: Add populateCache option to mutate (vercel#1729)
Browse files Browse the repository at this point in the history
* add populateCache option to mutate

* rename type and export it
  • Loading branch information
shuding authored and nevilm-lt committed Apr 22, 2022
1 parent 0112c5b commit df5fc9c
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 31 deletions.
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -21,6 +21,7 @@ export {
BareFetcher,
Fetcher,
MutatorCallback,
MutatorOptions,
Middleware,
Arguments
} from './types'
16 changes: 11 additions & 5 deletions src/types.ts
Expand Up @@ -142,13 +142,19 @@ export type MutatorCallback<Data = any> = (
currentValue?: Data
) => Promise<undefined | Data> | undefined | Data

export type MutatorOptions = {
revalidate?: boolean
populateCache?: boolean
}

export type Broadcaster<Data = any, Error = any> = (
cache: Cache<Data>,
key: string,
data: Data,
error?: Error,
isValidating?: boolean,
shouldRevalidate?: boolean
revalidate?: boolean,
populateCache?: boolean
) => Promise<Data>

export type State<Data, Error> = {
Expand All @@ -161,27 +167,27 @@ export type Mutator<Data = any> = (
cache: Cache,
key: Key,
data?: Data | Promise<Data> | MutatorCallback<Data>,
shouldRevalidate?: boolean
opts?: boolean | MutatorOptions
) => Promise<Data | undefined>

export interface ScopedMutator<Data = any> {
/** This is used for bound mutator */
(
key: Key,
data?: Data | Promise<Data> | MutatorCallback<Data>,
shouldRevalidate?: boolean
opts?: boolean | MutatorOptions
): Promise<Data | undefined>
/** This is used for global mutator */
<T = any>(
key: Key,
data?: T | Promise<T> | MutatorCallback<T>,
shouldRevalidate?: boolean
opts?: boolean | MutatorOptions
): Promise<T | undefined>
}

export type KeyedMutator<Data> = (
data?: Data | Promise<Data> | MutatorCallback<Data>,
shouldRevalidate?: boolean
opts?: boolean | MutatorOptions
) => Promise<Data | undefined>

// Public types
Expand Down
11 changes: 7 additions & 4 deletions src/utils/broadcast-state.ts
Expand Up @@ -8,7 +8,8 @@ export const broadcastState: Broadcaster = (
data,
error,
isValidating,
revalidate
revalidate,
populateCache = true
) => {
const [
EVENT_REVALIDATORS,
Expand All @@ -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.
Expand Down
53 changes: 31 additions & 22 deletions src/utils/mutate.ts
Expand Up @@ -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 <Data>(
...args: [
Cache,
Key,
undefined | Data | Promise<Data | undefined> | MutatorCallback<Data>,
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)
Expand All @@ -36,31 +42,33 @@ export const internalMutate = async <Data>(
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<Data>)(cache.get(key))
data = (data as MutatorCallback<Data>)(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<Data>).then)) {
// `data` is a promise/thenable, resolve the final data first.
if (data && isFunction((data as Promise<Data>).then)) {
// This means that the mutation is async, we need to check timestamps to
// avoid race conditions.
data = await (_data as Promise<Data>).catch(err => {
data = await (data as Promise<Data>).catch(err => {
error = err
})

Expand All @@ -71,16 +79,16 @@ export const internalMutate = async <Data>(
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()
Expand All @@ -92,10 +100,11 @@ export const internalMutate = async <Data>(
data,
error,
UNDEFINED,
revalidate
revalidate,
populateCache
)

// Throw error or return data
if (error) throw error
return res
return populateCache ? res : data
}
29 changes: 29 additions & 0 deletions test/use-swr-local-mutation.test.tsx
Expand Up @@ -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 (
<>
<div>data: {String(data)}</div>
<button
onClick={() =>
mutate('bar', {
revalidate: false,
populateCache: false
})
}
>
mutate
</button>
</>
)
}

renderWithConfig(<Page />)
await screen.findByText('data: foo')

fireEvent.click(screen.getByText('mutate'))
await sleep(30)
await screen.findByText('data: foo')
})
})

0 comments on commit df5fc9c

Please sign in to comment.