Skip to content
Open
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
19 changes: 18 additions & 1 deletion src/_internal/utils/merge-config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
import { mergeObjects } from './shared'
import type { FullConfiguration } from '../types'

// Strip keys with `undefined` values from an object so they don't override
// defaults when spread-merged. This prevents issues like passing
// `{ onSuccess: undefined }` from overriding the default `noop` callback.
const stripUndefinedValues = (obj: Partial<FullConfiguration>) => {
const result: Partial<FullConfiguration> = {}
for (const key in obj) {
if (obj[key as keyof FullConfiguration] !== undefined) {
;(result as any)[key] = obj[key as keyof FullConfiguration]
}
}
return result
}

export const mergeConfigs = (
a: Partial<FullConfiguration>,
b?: Partial<FullConfiguration>
) => {
// Need to create a new object to avoid mutating the original here.
const v: Partial<FullConfiguration> = mergeObjects(a, b)
// Strip undefined values from `b` so they don't override defaults from `a`.
const v: Partial<FullConfiguration> = mergeObjects(
a,
b ? stripUndefinedValues(b) : b
)

// If two configs are provided, merge their `use` and `fallback` options.
if (b) {
Expand Down
61 changes: 61 additions & 0 deletions test/use-swr-config-callbacks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,67 @@ describe('useSWR - config callbacks', () => {
expect(discardedEvents).toEqual([key])
})

it('should not cause infinite requests when onSuccess is explicitly undefined', async () => {
let count = 0
const key = createKey()
function Page() {
const { data } = useSWR(key, () => createResponse(count++), {
onSuccess: undefined,
errorRetryInterval: 50,
dedupingInterval: 0
})
return <div>data: {data}</div>
}
renderWithConfig(<Page />)
await screen.findByText('data: 0')

// Wait longer than the retry interval. If onSuccess: undefined caused a
// TypeError that triggered error-retry cycles, count would exceed 1.
await act(() => sleep(200))
expect(count).toBe(1)
})

it('should not cause infinite requests when onError is explicitly undefined', async () => {
let count = 0
const key = createKey()
function Page() {
const { data } = useSWR(key, () => createResponse(count++), {
onError: undefined,
errorRetryInterval: 50,
dedupingInterval: 0
})
return <div>data: {data}</div>
}
renderWithConfig(<Page />)
await screen.findByText('data: 0')

await act(() => sleep(200))
expect(count).toBe(1)
})

it('should handle a custom hook passing optional onSuccess that is undefined', async () => {
let count = 0
const key = createKey()

function useMyFetch({ onSuccess }: { onSuccess?: (data: any) => void }) {
return useSWR(key, () => createResponse(count++), {
onSuccess,
errorRetryInterval: 50,
dedupingInterval: 0
})
}

function Page() {
const { data } = useMyFetch({})
return <div>data: {data}</div>
}
renderWithConfig(<Page />)
await screen.findByText('data: 0')

await act(() => sleep(200))
expect(count).toBe(1)
})

it('should not trigger the onSuccess callback when discarded', async () => {
const key = createKey()
const discardedEvents = []
Expand Down