Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add populateCache option to mutate #1729

Merged
merged 2 commits into from Dec 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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')
})
})