diff --git a/src/use-swr.ts b/src/use-swr.ts index 194d46d05..0410368e4 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -140,9 +140,13 @@ export const useSWRHandler = ( 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. @@ -184,12 +188,7 @@ export const useSWRHandler = ( 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()) { @@ -262,7 +261,9 @@ export const useSWRHandler = ( 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({ diff --git a/src/utils/broadcast-state.ts b/src/utils/broadcast-state.ts index 6ff826110..2ce246c80 100644 --- a/src/utils/broadcast-state.ts +++ b/src/utils/broadcast-state.ts @@ -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] @@ -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) diff --git a/src/utils/mutate.ts b/src/utils/mutate.ts index 9d490e0b7..12e6396f4 100644 --- a/src/utils/mutate.ts +++ b/src/utils/mutate.ts @@ -19,8 +19,9 @@ export const internalMutate = async ( 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, diff --git a/test/use-swr-integration.test.tsx b/test/use-swr-integration.test.tsx index 8c2ffa9de..131754993 100644 --- a/test/use-swr-integration.test.tsx +++ b/test/use-swr-integration.test.tsx @@ -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 ? : null} + + + ) + } + + renderWithConfig() + 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') + }) }) diff --git a/test/use-swr-revalidate.test.tsx b/test/use-swr-revalidate.test.tsx index 4cf0ea3e6..7406f3717 100644 --- a/test/use-swr-revalidate.test.tsx +++ b/test/use-swr-revalidate.test.tsx @@ -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', () => { @@ -162,4 +163,39 @@ describe('useSWR - revalidate', () => { renderWithConfig() 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 ? : null} + + + + ) + } + + renderWithConfig() + 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') + }) })