diff --git a/src/_internal/types.ts b/src/_internal/types.ts index c5dee2f2e..e93fe18bf 100644 --- a/src/_internal/types.ts +++ b/src/_internal/types.ts @@ -221,6 +221,11 @@ export interface PublicConfiguration< * @see {@link https://swr.vercel.app/docs/revalidation#disable-automatic-revalidations} */ revalidateIfStale: boolean + /** + * automatically revalidate when data comes from RSC fallback + * @defaultValue false + */ + revalidateOnRSCFallback?: boolean /** * retry when fetcher has an error * @defaultValue true diff --git a/src/index/use-swr.ts b/src/index/use-swr.ts index bb9b011af..9a772235e 100644 --- a/src/index/use-swr.ts +++ b/src/index/use-swr.ts @@ -219,6 +219,14 @@ export const useSWRHandler = ( if (isInitialMount && !isUndefined(revalidateOnMount)) return revalidateOnMount const data = !isUndefined(fallback) ? fallback : snapshot.data + // If revalidateOnRSCFallback is explicitly configured, use it for fallback data + if ( + !isUndefined(fallback) && + !isUndefined(data) && + isInitialMount && + !isUndefined(getConfig().revalidateOnRSCFallback) + ) + return getConfig().revalidateOnRSCFallback if (suspense) return isUndefined(data) || revalidateIfStale return isUndefined(data) || revalidateIfStale })() @@ -367,6 +375,16 @@ export const useSWRHandler = ( // If `revalidateOnMount` is set, we take the value directly. if (isInitialMount && !isUndefined(revalidateOnMount)) return revalidateOnMount + + // If revalidateOnRSCFallback is explicitly configured, use it for fallback data + if ( + !isUndefined(fallback) && + !isUndefined(data) && + !IS_SERVER && + isInitialMount && + !isUndefined(getConfig().revalidateOnRSCFallback) + ) + return getConfig().revalidateOnRSCFallback // Under suspense mode, it will always fetch on render if there is no // stale data so no need to revalidate immediately mount it again. // If data exists, only revalidate if `revalidateIfStale` is true. diff --git a/test/rsc-fallback.test.tsx b/test/rsc-fallback.test.tsx new file mode 100644 index 000000000..5138a9214 --- /dev/null +++ b/test/rsc-fallback.test.tsx @@ -0,0 +1,99 @@ +import { act, screen } from '@testing-library/react' +import React from 'react' +import useSWR from 'swr' +import { createKey, renderWithConfig, sleep } from './utils' + +describe('RSC Fallback', () => { + it('should not revalidate on mount with RSC fallback data when revalidateOnRSCFallback is false', async () => { + const key = createKey() + const fetchFn = jest.fn(() => 'updated data') + + function Page() { + const { data, isValidating, isLoading } = useSWR(key, fetchFn, { + fallbackData: 'RSC data', + revalidateOnRSCFallback: false + }) + return ( +
+ Data: {data}, isValidating: {String(isValidating)}, isLoading:{' '} + {String(isLoading)} +
+ ) + } + + renderWithConfig() + + screen.getByText(content => content.includes('Data: RSC data')) + screen.getByText(content => content.includes('isValidating: false')) + screen.getByText(content => content.includes('isLoading: false')) + expect(fetchFn).not.toHaveBeenCalled() + + await act(() => sleep(50)) + + expect(fetchFn).not.toHaveBeenCalled() + }) + + it('should revalidate on mount with RSC fallback data when revalidateOnRSCFallback is true', async () => { + const key = createKey() + const fetchFn = jest.fn(() => 'updated data') + + function Page() { + const { data } = useSWR(key, fetchFn, { + fallbackData: 'RSC data', + revalidateOnRSCFallback: true + }) + return
Data: {data}
+ } + + renderWithConfig() + + expect(screen.getByText('Data: RSC data')).toBeInTheDocument() + + await act(() => sleep(50)) + + expect(fetchFn).toHaveBeenCalledTimes(1) + expect(screen.getByText('Data: updated data')).toBeInTheDocument() + }) + + it('should not affect existing fallback behavior when revalidateOnRSCFallback is not set', async () => { + const key = createKey() + const fetchFn = jest.fn(() => 'updated data') + + function Page() { + const { data } = useSWR(key, fetchFn, { + fallbackData: 'fallback data' + }) + return
Data: {data}
+ } + + renderWithConfig() + + expect(screen.getByText('Data: fallback data')).toBeInTheDocument() + + // Default behavior: revalidateIfStale is true, so it should revalidate + await act(() => sleep(50)) + + expect(fetchFn).toHaveBeenCalledTimes(1) + expect(screen.getByText('Data: updated data')).toBeInTheDocument() + }) + + it('should work with SWRConfig provider fallback when revalidateOnRSCFallback is false', async () => { + const key = createKey() + const fetchFn = jest.fn(() => 'updated data') + + function Page() { + const { data } = useSWR(key, fetchFn, { + revalidateOnRSCFallback: false + }) + return
Data: {data}
+ } + + renderWithConfig(, { fallback: { [key]: 'provider fallback' } }) + + expect(screen.getByText('Data: provider fallback')).toBeInTheDocument() + + await act(() => sleep(50)) + + expect(fetchFn).not.toHaveBeenCalled() + }) +})