diff --git a/src/_internal/utils/mutate.ts b/src/_internal/utils/mutate.ts index 071af7803..f7a309610 100644 --- a/src/_internal/utils/mutate.ts +++ b/src/_internal/utils/mutate.ts @@ -106,10 +106,12 @@ export async function internalMutate( // requests will not be deduped. delete FETCH[key] delete PRELOAD[key] - if (revalidators && revalidators[0]) { - return revalidators[0](revalidateEvents.MUTATE_EVENT).then( - () => get().data - ) + if (revalidators && revalidators.length) { + return Promise.all( + revalidators.map(r => + r(revalidateEvents.MUTATE_EVENT, { dedupe: true }) + ) + ).then(() => get().data) } } return get().data diff --git a/src/index/use-swr.ts b/src/index/use-swr.ts index bb9b011af..943e4d95c 100644 --- a/src/index/use-swr.ts +++ b/src/index/use-swr.ts @@ -89,6 +89,23 @@ type DefinitelyTruthy = false extends T ? never : T +function triggerRevalidators( + revalidators: + | (( + type: RevalidateEvent, + opts?: { retryCount?: number; dedupe?: boolean } + ) => void)[] + | undefined, + type: RevalidateEvent, + opts?: { retryCount?: number; dedupe?: boolean } +) { + if (revalidators) { + for (const revalidate of revalidators) { + revalidate(type, opts) + } + } +} + const resolvedUndef = Promise.resolve(UNDEFINED) const sub = () => noop /** @@ -581,13 +598,11 @@ export const useSWRHandler = ( key, currentConfig, _opts => { - const revalidators = EVENT_REVALIDATORS[key] - if (revalidators && revalidators[0]) { - revalidators[0]( - revalidateEvents.ERROR_REVALIDATE_EVENT, - _opts - ) - } + triggerRevalidators( + EVENT_REVALIDATORS[key], + revalidateEvents.ERROR_REVALIDATE_EVENT, + _opts + ) }, { retryCount: (opts.retryCount || 0) + 1, @@ -681,7 +696,7 @@ export const useSWRHandler = ( softRevalidate() } } else if (type == revalidateEvents.MUTATE_EVENT) { - return revalidate() + return revalidate(opts) } else if (type == revalidateEvents.ERROR_REVALIDATE_EVENT) { return revalidate(opts) } diff --git a/test/use-swr-revalidate.test.tsx b/test/use-swr-revalidate.test.tsx index b36a04f09..f72019c8f 100644 --- a/test/use-swr-revalidate.test.tsx +++ b/test/use-swr-revalidate.test.tsx @@ -198,4 +198,144 @@ describe('useSWR - revalidate', () => { await act(() => sleep(20)) screen.getByText('data: 1') }) + + it('should revalidate when one of multiple hooks with the same key has isPaused returning false', async () => { + const key = createKey() + let value = 0 + const fetcher = () => createResponse(value++, { delay: 10 }) + + // A component whose isPaused is always true (simulates unfocused screen) + function PausedComponent() { + const { data } = useSWR(key, fetcher, { + isPaused: () => true, + dedupingInterval: 0 + }) + return paused:{String(data ?? '')} + } + + // A component whose isPaused is always false (simulates focused screen) + function ActiveComponent() { + const { data, mutate } = useSWR(key, fetcher, { + isPaused: () => false, + dedupingInterval: 0 + }) + return mutate()}>active:{String(data ?? '')} + } + + function Page() { + return ( + <> + + + + ) + } + + renderWithConfig() + + // The active (unpaused) hook should fetch data despite the paused one + await screen.findByText('active:0') + + // Trigger a mutate — should revalidate via the active hook + fireEvent.click(screen.getByText('active:0')) + await act(() => sleep(50)) + await screen.findByText('active:1') + }) + + it('should call all revalidators on mutate when multiple hooks share a key', async () => { + const key = createKey() + let value = 0 + const fetcher = () => createResponse(value++, { delay: 10 }) + + function HookA() { + const { data } = useSWR(key, fetcher, { dedupingInterval: 0 }) + return a:{String(data ?? '')} + } + + function HookB() { + const { data, mutate } = useSWR(key, fetcher, { dedupingInterval: 0 }) + return mutate()}>b:{String(data ?? '')} + } + + function Page() { + return ( + <> + + + + ) + } + + renderWithConfig() + + // Both hooks should get initial data + await screen.findByText('a:0') + await screen.findByText('b:0') + + // Trigger mutate — all revalidators should fire + fireEvent.click(screen.getByText('b:0')) + await act(() => sleep(50)) + await screen.findByText('a:1') + await screen.findByText('b:1') + }) + + it('should call all revalidators on error retry when multiple hooks share a key', async () => { + const key = createKey() + let callCount = 0 + + function HookA() { + const { data, error } = useSWR( + key, + () => { + callCount++ + if (callCount <= 2) throw new Error('fail') + return createResponse('recovered', { delay: 10 }) + }, + { + isPaused: () => false, + dedupingInterval: 0, + errorRetryInterval: 50, + errorRetryCount: 3 + } + ) + return a:{error ? 'error' : String(data ?? '')} + } + + function HookB() { + const { data, error } = useSWR( + key, + () => { + callCount++ + if (callCount <= 2) throw new Error('fail') + return createResponse('recovered', { delay: 10 }) + }, + { + isPaused: () => false, + dedupingInterval: 0, + errorRetryInterval: 50, + errorRetryCount: 3 + } + ) + return b:{error ? 'error' : String(data ?? '')} + } + + function Page() { + return ( + <> + + + + ) + } + + renderWithConfig() + + // Initially both should error + await screen.findByText('a:error') + + // After retry, both should recover + await act(() => sleep(200)) + await screen.findByText('a:recovered') + await screen.findByText('b:recovered') + }) })