Skip to content

Commit

Permalink
fix: Invalidate the key when mutating without revalidating (#1498)
Browse files Browse the repository at this point in the history
* add test for mount revalidation and deduplication

* clear dedupling promises when invalidating a key

* delete concurrent promise when revalidate is set to true

Co-authored-by: Shu Ding <g@shud.in>
Co-authored-by: Gang Chen <13298548+MoonBall@users.noreply.github.com>

* disable ts assertion

Co-authored-by: Gang Chen <13298548+MoonBall@users.noreply.github.com>
  • Loading branch information
shuding and MoonBall authored Sep 27, 2021
1 parent b6de933 commit 6746f49
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 20 deletions.
21 changes: 11 additions & 10 deletions src/use-swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,13 @@ export const useSWRHandler = <Data = any, Error = any>(
key === keyRef.current &&
initialMountedRef.current

const cleanupState = () => {
delete CONCURRENT_PROMISES[key]
delete CONCURRENT_PROMISES_TS[key]
const cleanupState = (ts: number) => {
// CONCURRENT_PROMISES_TS[key] might be overridden, check if it's still
// the same request before deleting.
if (CONCURRENT_PROMISES_TS[key] === ts) {
delete CONCURRENT_PROMISES[key]
delete CONCURRENT_PROMISES_TS[key]
}
}

// Start fetching. Change the `isValidating` state, update the cache.
Expand Down Expand Up @@ -184,12 +188,7 @@ export const useSWRHandler = <Data = any, Error = any>(
if (shouldStartNewRequest) {
// If the request isn't interrupted, clean it up after the
// deduplication interval.
setTimeout(() => {
// CONCURRENT_PROMISES_TS[key] maybe be `undefined`.
if (CONCURRENT_PROMISES_TS[key] === startAt) {
cleanupState()
}
}, config.dedupingInterval)
setTimeout(() => cleanupState(startAt), config.dedupingInterval)

// Trigger the successful callback.
if (isCallbackSafe()) {
Expand Down Expand Up @@ -262,7 +261,9 @@ export const useSWRHandler = <Data = any, Error = any>(
broadcastState(cache, key, newData, newState.error, false)
}
} catch (err) {
cleanupState()
// Reset the state immediately.
// @ts-ignore
cleanupState(startAt)
cache.set(keyValidating, false)
if (getConfig().isPaused()) {
setState({
Expand Down
26 changes: 19 additions & 7 deletions src/utils/broadcast-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ export const broadcastState: Broadcaster = (
isValidating,
revalidate
) => {
const [EVENT_REVALIDATORS, STATE_UPDATERS] = SWRGlobalState.get(
cache
) as GlobalState
const [
EVENT_REVALIDATORS,
STATE_UPDATERS,
,
,
CONCURRENT_PROMISES,
CONCURRENT_PROMISES_TS
] = SWRGlobalState.get(cache) as GlobalState
const revalidators = EVENT_REVALIDATORS[key]
const updaters = STATE_UPDATERS[key]

Expand All @@ -24,10 +29,17 @@ export const broadcastState: Broadcaster = (
}

// If we also need to revalidate, only do it for the first hook.
if (revalidate && revalidators && revalidators[0]) {
return revalidators[0](revalidateEvents.MUTATE_EVENT).then(() =>
cache.get(key)
)
if (revalidate) {
// Invalidate the key by deleting the concurrent request markers so new
// requests will not be deduped.
delete CONCURRENT_PROMISES[key]
delete CONCURRENT_PROMISES_TS[key]

if (revalidators && revalidators[0]) {
return revalidators[0](revalidateEvents.MUTATE_EVENT).then(() =>
cache.get(key)
)
}
}

return cache.get(key)
Expand Down
3 changes: 2 additions & 1 deletion src/utils/mutate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ export const internalMutate = async <Data>(
cache
) as GlobalState

// if there is no new data to update, let's just revalidate the key
// If there is no new data to update, we revalidate the key.
if (isUndefined(_data)) {
// Revalidate and broadcast state.
return broadcastState(
cache,
key,
Expand Down
31 changes: 31 additions & 0 deletions test/use-swr-integration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -373,4 +373,35 @@ describe('useSWR', () => {
expect(fetcher).toBeCalled()
await screen.findByText('hello, SWR')
})

it('should revalidate on mount after dedupingInterval', async () => {
const key = createKey()
let cnt = 0

function Foo() {
const { data } = useSWR(key, () => 'data: ' + cnt++, {
dedupingInterval: 0
})
return <>{data}</>
}

function Page() {
const [showFoo, setShowFoo] = React.useState(true)
return (
<>
{showFoo ? <Foo /> : null}
<button onClick={() => setShowFoo(!showFoo)}>toggle</button>
</>
)
}

renderWithConfig(<Page />)
await waitForNextTick()
screen.getByText('data: 0')
fireEvent.click(screen.getByText('toggle'))
await waitForNextTick()
fireEvent.click(screen.getByText('toggle'))
await act(() => sleep(20))
screen.getByText('data: 1')
})
})
40 changes: 38 additions & 2 deletions test/use-swr-revalidate.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { act, fireEvent, screen } from '@testing-library/react'
import React from 'react'
import useSWR from 'swr'
import useSWR, { useSWRConfig } from 'swr'
import {
createResponse,
sleep,
nextTick as waitForNextTick,
createKey,
renderWithConfig
renderWithConfig,
nextTick
} from './utils'

describe('useSWR - revalidate', () => {
Expand Down Expand Up @@ -162,4 +163,39 @@ describe('useSWR - revalidate', () => {
renderWithConfig(<Page />)
screen.getByText('false')
})

it('should mark the key as invalidated and clear deduping with `mutate`, even if there is no mounted hook', async () => {
const key = createKey()
let cnt = 0

function Foo() {
const { data } = useSWR(key, () => 'data: ' + cnt++, {
dedupingInterval: 1000
})
return <>{data}</>
}

function Page() {
const [showFoo, setShowFoo] = React.useState(true)
const { mutate } = useSWRConfig()
return (
<>
{showFoo ? <Foo /> : null}
<button onClick={() => setShowFoo(!showFoo)}>toggle</button>
<button onClick={() => mutate(key)}>mutate</button>
</>
)
}

renderWithConfig(<Page />)
await nextTick()
screen.getByText('data: 0')
fireEvent.click(screen.getByText('toggle'))
await nextTick()
fireEvent.click(screen.getByText('mutate'))
await nextTick()
fireEvent.click(screen.getByText('toggle'))
await act(() => sleep(20))
screen.getByText('data: 1')
})
})

0 comments on commit 6746f49

Please sign in to comment.