Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,11 @@ export type MutatorCallback<Data = any> = (
currentValue?: Data
) => Promise<undefined | Data> | undefined | Data

export type MutatorOptions = {
export type MutatorOptions<Data = any> = {
revalidate?: boolean
populateCache?: boolean
optimisticData?: Data
rollbackOnError?: boolean
}

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

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

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

// Public types
Expand Down
6 changes: 3 additions & 3 deletions src/utils/broadcast-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ export const broadcastState: Broadcaster = (
error,
isValidating,
revalidate,
populateCache = true
broadcast = true
) => {
const [EVENT_REVALIDATORS, STATE_UPDATERS, , , CONCURRENT_REQUESTS] =
SWRGlobalState.get(cache) as GlobalState
const revalidators = EVENT_REVALIDATORS[key]
const updaters = STATE_UPDATERS[key] || []
const updaters = STATE_UPDATERS[key]

// Cache was populated, update states of all hooks.
if (populateCache && updaters) {
if (broadcast && updaters) {
for (let i = 0; i < updaters.length; ++i) {
updaters[i](data, error, isValidating)
}
Expand Down
21 changes: 18 additions & 3 deletions src/utils/mutate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { serialize } from './serialize'
import { isFunction, UNDEFINED } from './helper'
import { isFunction, isUndefined, UNDEFINED } from './helper'
import { SWRGlobalState, GlobalState } from './global-state'
import { broadcastState } from './broadcast-state'
import { getTimestamp } from './timestamp'
Expand All @@ -11,7 +11,7 @@ export const internalMutate = async <Data>(
Cache,
Key,
undefined | Data | Promise<Data | undefined> | MutatorCallback<Data>,
undefined | boolean | MutatorOptions
undefined | boolean | MutatorOptions<Data>
]
) => {
const [cache, _key, _data, _opts] = args
Expand All @@ -22,8 +22,10 @@ export const internalMutate = async <Data>(
typeof _opts === 'boolean' ? { revalidate: _opts } : _opts || {}

// Fallback to `true` if it's not explicitly set to `false`
let populateCache = options.populateCache !== false
const revalidate = options.revalidate !== false
const populateCache = options.populateCache !== false
const rollbackOnError = options.rollbackOnError !== false
const optimisticData = options.optimisticData

// Serilaize key
const [key, , keyErr] = serialize(_key)
Expand Down Expand Up @@ -53,6 +55,14 @@ export const internalMutate = async <Data>(
// Update global timestamps.
const beforeMutationTs = (MUTATION_TS[key] = getTimestamp())
MUTATION_END_TS[key] = 0
const hasOptimisticData = !isUndefined(optimisticData)
const rollbackData = cache.get(key)

// Do optimistic data update.
if (hasOptimisticData) {
cache.set(key, optimisticData)
broadcastState(cache, key, optimisticData)
}

if (isFunction(data)) {
// `data` is a function, call it passing current cache value.
Expand All @@ -78,6 +88,11 @@ export const internalMutate = async <Data>(
if (beforeMutationTs !== MUTATION_TS[key]) {
if (error) throw error
return data
} else if (error && hasOptimisticData && rollbackOnError) {
// Rollback. Always populate the cache in this case.
populateCache = true
data = rollbackData
cache.set(key, rollbackData)
}
}

Expand Down
101 changes: 101 additions & 0 deletions test/use-swr-local-mutation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1011,4 +1011,105 @@ describe('useSWR - local mutation', () => {
await sleep(30)
await screen.findByText('data: foo')
})

it('should support optimistic updates via `optimisticData`', async () => {
const key = createKey()
const renderedData = []
let mutate

function Page() {
const { data, mutate: boundMutate } = useSWR(key, () =>
createResponse('foo', { delay: 20 })
)
mutate = boundMutate
renderedData.push(data)
return <div>data: {String(data)}</div>
}

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

await act(() =>
mutate(createResponse('baz', { delay: 20 }), {
optimisticData: 'bar'
})
)
await sleep(30)
expect(renderedData).toEqual([undefined, 'foo', 'bar', 'baz', 'foo'])
})

it('should rollback optimistic updates when mutation fails', async () => {
const key = createKey()
const renderedData = []
let mutate
let cnt = 0

function Page() {
const { data, mutate: boundMutate } = useSWR(key, () =>
createResponse(cnt++, { delay: 20 })
)
mutate = boundMutate
if (
!renderedData.length ||
renderedData[renderedData.length - 1] !== data
) {
renderedData.push(data)
}
return <div>data: {String(data)}</div>
}

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

try {
await act(() =>
mutate(createResponse(new Error('baz'), { delay: 20 }), {
optimisticData: 'bar'
})
)
} catch (e) {
expect(e.message).toEqual('baz')
}

await sleep(30)
expect(renderedData).toEqual([undefined, 0, 'bar', 0, 1])
})

it('should not rollback optimistic updates if `rollbackOnError`', async () => {
const key = createKey()
const renderedData = []
let mutate
let cnt = 0

function Page() {
const { data, mutate: boundMutate } = useSWR(key, () =>
createResponse(cnt++, { delay: 20 })
)
mutate = boundMutate
if (
!renderedData.length ||
renderedData[renderedData.length - 1] !== data
) {
renderedData.push(data)
}
return <div>data: {String(data)}</div>
}

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

try {
await act(() =>
mutate(createResponse(new Error('baz'), { delay: 20 }), {
optimisticData: 'bar',
rollbackOnError: false
})
)
} catch (e) {
expect(e.message).toEqual('baz')
}

await sleep(30)
expect(renderedData).toEqual([undefined, 0, 'bar', 1])
})
})