Skip to content

Commit

Permalink
Fix race conditions related to optimistic UI (#1970)
Browse files Browse the repository at this point in the history
* fix optimistic ui race conditions

* fix test

* fix
  • Loading branch information
shuding committed May 15, 2022
1 parent 4e313d5 commit 2dd9451
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 9 deletions.
27 changes: 19 additions & 8 deletions _internal/utils/mutate.ts
@@ -1,5 +1,5 @@
import { serialize } from './serialize'
import { createCacheHelper, isFunction, isUndefined } from './helper'
import { createCacheHelper, isFunction, isUndefined, UNDEFINED } from './helper'
import { SWRGlobalState } from './global-state'
import { getTimestamp } from './timestamp'
import * as revalidateEvents from '../constants'
Expand All @@ -8,7 +8,8 @@ import {
Cache,
MutatorCallback,
MutatorOptions,
GlobalState
GlobalState,
State
} from '../types'

export const internalMutate = async <Data>(
Expand Down Expand Up @@ -38,12 +39,19 @@ export const internalMutate = async <Data>(
const [key] = serialize(_key)
if (!key) return

const [get, set] = createCacheHelper<Data>(cache, key)
const [get, set] = createCacheHelper<
Data,
State<Data, any> & {
// The original data.
_o?: Data
}
>(cache, key)
const [EVENT_REVALIDATORS, MUTATION, FETCH] = SWRGlobalState.get(
cache
) as GlobalState

const revalidators = EVENT_REVALIDATORS[key]
const startRevalidate = async () => {
const startRevalidate = () => {
if (revalidate) {
// Invalidate the key by deleting the concurrent request markers so new
// requests will not be deduped.
Expand All @@ -56,6 +64,7 @@ export const internalMutate = async <Data>(
}
return get().data
}

// If there is no new data provided, revalidate the key with current state.
if (args.length < 3) {
// Revalidate and broadcast state.
Expand All @@ -70,14 +79,16 @@ export const internalMutate = async <Data>(
MUTATION[key] = [beforeMutationTs, 0]

const hasOptimisticData = !isUndefined(optimisticData)
const originalData = get().data
const state = get()
const currentData = state.data
const originalData = isUndefined(state._o) ? currentData : state._o

// Do optimistic data update.
if (hasOptimisticData) {
optimisticData = isFunction(optimisticData)
? optimisticData(originalData)
: optimisticData
set({ data: optimisticData })
set({ data: optimisticData, _o: originalData })
}

if (isFunction(data)) {
Expand Down Expand Up @@ -125,8 +136,8 @@ export const internalMutate = async <Data>(
set({ data })
}

// Always update or reset the error.
set({ error })
// Always update or reset the error and original data field.
set({ error, _o: UNDEFINED })
}

// Reset the timestamp to mark the mutation has ended.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -82,7 +82,7 @@
"lint": "eslint . --ext .ts,.tsx --cache",
"lint:fix": "yarn lint --fix",
"coverage": "jest --coverage",
"test": "jest && tsc --noEmit -p test"
"test": "tsc --noEmit -p test && jest"
},
"husky": {
"hooks": {
Expand Down
56 changes: 56 additions & 0 deletions test/use-swr-local-mutation.test.tsx
Expand Up @@ -1142,6 +1142,62 @@ describe('useSWR - local mutation', () => {
expect(renderedData).toEqual([undefined, 0, 'bar', 0, 1])
})

it('should not revert to optimistic data when rolling back', async () => {
const key = createKey()
const renderedData = []
let mutate
let previousValue
let previousValue2

function Page() {
const { data, mutate: boundMutate } = useSWR(key, () =>
createResponse(0, { 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')

await executeWithoutBatching(async () => {
const p1 = mutate(createResponse(new Error(), { delay: 20 }), {
optimisticData: 1
})
await sleep(10)
const p2 = mutate(
v => {
previousValue = v
return createResponse(new Error(), { delay: 20 })
},
{
optimisticData: v => {
previousValue2 = v
return 2
}
}
)
return Promise.all([p1, p2])
}).catch(_e => {})

await sleep(30)

// It should revert to `0` instead of `1` at the end.
expect(renderedData).toEqual([undefined, 0, 1, 2, 0])

// It should receive the original displayed data instead of current displayed data.
expect(previousValue).toBe(0)
expect(previousValue2).toBe(0)
})

it('should not rollback optimistic updates if `rollbackOnError`', async () => {
const key = createKey()
const renderedData = []
Expand Down

0 comments on commit 2dd9451

Please sign in to comment.