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

fix: Invalidate the key when mutating without revalidating #1498

Merged
merged 5 commits into from
Sep 27, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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')
})
})