From 755d38f831fd6bd0773f045461728ce79ce90c4b Mon Sep 17 00:00:00 2001 From: David Condrey Date: Thu, 17 Apr 2025 06:31:16 -0700 Subject: [PATCH 1/3] Update use-swr.ts Call all revalidators for a key to respect individual isPaused() states (#2333) --- src/index/use-swr.ts | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/index/use-swr.ts b/src/index/use-swr.ts index 21c42a9c2..2fab553ae 100644 --- a/src/index/use-swr.ts +++ b/src/index/use-swr.ts @@ -89,6 +89,25 @@ type DefinitelyTruthy = false extends T ? never : T +function triggerRevalidators( + key: string, + revalidators: ((type: RevalidateEvent, opts?: { retryCount?: number; dedupe?: boolean }) => void)[] | undefined, + type: RevalidateEvent, + opts?: { retryCount?: number; dedupe?: boolean } +) { + if (!Array.isArray(revalidators)) return + + for (const revalidate of revalidators) { + try { + revalidate(type, opts) + } catch (err) { + if (process.env.NODE_ENV !== 'production') { + console.error(`SWR: Error triggering revalidator for key "${key}"`, err) + } + } + } +} + export const useSWRHandler = ( _key: Key, fetcher: Fetcher | null, @@ -521,12 +540,12 @@ export const useSWRHandler = ( currentConfig, _opts => { const revalidators = EVENT_REVALIDATORS[key] - if (revalidators && revalidators[0]) { - revalidators[0]( - revalidateEvents.ERROR_REVALIDATE_EVENT, - _opts - ) - } + triggerRevalidators( + key, + EVENT_REVALIDATORS[key], + revalidateEvents.ERROR_REVALIDATE_EVENT, + _opts + ) }, { retryCount: (opts.retryCount || 0) + 1, From 4f23ae0709f793965828fbe6cfd03bb1bad5b979 Mon Sep 17 00:00:00 2001 From: David Condrey Date: Thu, 17 Apr 2025 06:51:38 -0700 Subject: [PATCH 2/3] Update use-swr.ts Use the revalidators variable --- src/index/use-swr.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index/use-swr.ts b/src/index/use-swr.ts index 2fab553ae..68301c604 100644 --- a/src/index/use-swr.ts +++ b/src/index/use-swr.ts @@ -542,7 +542,7 @@ export const useSWRHandler = ( const revalidators = EVENT_REVALIDATORS[key] triggerRevalidators( key, - EVENT_REVALIDATORS[key], + revalidators, revalidateEvents.ERROR_REVALIDATE_EVENT, _opts ) From 41a793047223f5c9db923d9f5779bf97fa5c6de1 Mon Sep 17 00:00:00 2001 From: David Condrey Date: Sat, 7 Mar 2026 20:04:32 -0800 Subject: [PATCH 3/3] fix: call all revalidators for a key to respect individual isPaused() states (#2333) --- src/_internal/utils/mutate.ts | 10 ++- src/index/use-swr.ts | 25 +++--- test/use-swr-revalidate.test.tsx | 140 +++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 18 deletions(-) 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 b389b4b19..271ed8267 100644 --- a/src/index/use-swr.ts +++ b/src/index/use-swr.ts @@ -90,23 +90,22 @@ type DefinitelyTruthy = false extends T : T function triggerRevalidators( - key: string, - revalidators: ((type: RevalidateEvent, opts?: { retryCount?: number; dedupe?: boolean }) => void)[] | undefined, + revalidators: + | (( + type: RevalidateEvent, + opts?: { retryCount?: number; dedupe?: boolean } + ) => void)[] + | undefined, type: RevalidateEvent, opts?: { retryCount?: number; dedupe?: boolean } ) { - if (!Array.isArray(revalidators)) return - - for (const revalidate of revalidators) { - try { + if (revalidators) { + for (const revalidate of revalidators) { revalidate(type, opts) - } catch (err) { - if (process.env.NODE_ENV !== 'production') { - console.error(`SWR: Error triggering revalidator for key "${key}"`, err) - } } } } + const resolvedUndef = Promise.resolve(UNDEFINED) /** @@ -598,10 +597,8 @@ export const useSWRHandler = ( key, currentConfig, _opts => { - const revalidators = EVENT_REVALIDATORS[key] triggerRevalidators( - key, - revalidators, + EVENT_REVALIDATORS[key], revalidateEvents.ERROR_REVALIDATE_EVENT, _opts ) @@ -698,7 +695,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') + }) })