Skip to content

Commit fb13a05

Browse files
committed
Merge remote-tracking branch 'react-query/master' into v4
2 parents b12bc2c + dc2df10 commit fb13a05

File tree

3 files changed

+111
-0
lines changed

3 files changed

+111
-0
lines changed

src/core/query.ts

+2
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,8 @@ export class Query<
363363
// Silently cancel current fetch if the user wants to cancel refetches
364364
this.cancel({ silent: true })
365365
} else if (this.promise) {
366+
// make sure that retries that were potentially cancelled due to unmounts can continue
367+
this.retryer?.continueRetry()
366368
// Return current promise if we are already fetching
367369
return this.promise
368370
}

src/core/retryer.ts

+4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export function isCancelledError(value: any): value is CancelledError {
5252
export class Retryer<TData = unknown, TError = unknown> {
5353
cancel: (options?: CancelOptions) => void
5454
cancelRetry: () => void
55+
continueRetry: () => void
5556
continue: () => void
5657
failureCount: number
5758
isPaused: boolean
@@ -72,6 +73,9 @@ export class Retryer<TData = unknown, TError = unknown> {
7273
this.cancelRetry = () => {
7374
cancelRetry = true
7475
}
76+
this.continueRetry = () => {
77+
cancelRetry = false
78+
}
7579
this.continue = () => continueFn?.()
7680
this.failureCount = 0
7781
this.isPaused = false

src/reactjs/tests/useQuery.test.tsx

+105
Original file line numberDiff line numberDiff line change
@@ -2711,6 +2711,111 @@ describe('useQuery', () => {
27112711
consoleMock.mockRestore()
27122712
})
27132713

2714+
it('should continue retries when observers unmount and remount while waiting for a retry (#3031)', async () => {
2715+
const key = queryKey()
2716+
const consoleMock = mockConsoleError()
2717+
let count = 0
2718+
2719+
function Page() {
2720+
const result = useQuery(
2721+
key,
2722+
async () => {
2723+
count++
2724+
await sleep(10)
2725+
return Promise.reject('some error')
2726+
},
2727+
{
2728+
retry: 2,
2729+
retryDelay: 100,
2730+
}
2731+
)
2732+
2733+
return (
2734+
<div>
2735+
<div>error: {result.error ?? 'null'}</div>
2736+
<div>failureCount: {result.failureCount}</div>
2737+
</div>
2738+
)
2739+
}
2740+
2741+
function App() {
2742+
const [show, toggle] = React.useReducer(x => !x, true)
2743+
2744+
return (
2745+
<div>
2746+
<button onClick={toggle}>{show ? 'hide' : 'show'}</button>
2747+
{show && <Page />}
2748+
</div>
2749+
)
2750+
}
2751+
2752+
const rendered = renderWithClient(queryClient, <App />)
2753+
2754+
await waitFor(() => rendered.getByText('failureCount: 1'))
2755+
rendered.getByRole('button', { name: /hide/i }).click()
2756+
rendered.getByRole('button', { name: /show/i }).click()
2757+
await waitFor(() => rendered.getByText('error: some error'))
2758+
2759+
expect(count).toBe(3)
2760+
2761+
consoleMock.mockRestore()
2762+
})
2763+
2764+
it('should restart when observers unmount and remount while waiting for a retry when query was cancelled in between (#3031)', async () => {
2765+
const key = queryKey()
2766+
const consoleMock = mockConsoleError()
2767+
let count = 0
2768+
2769+
function Page() {
2770+
const result = useQuery(
2771+
key,
2772+
async () => {
2773+
count++
2774+
await sleep(10)
2775+
return Promise.reject('some error')
2776+
},
2777+
{
2778+
retry: 2,
2779+
retryDelay: 100,
2780+
}
2781+
)
2782+
2783+
return (
2784+
<div>
2785+
<div>error: {result.error ?? 'null'}</div>
2786+
<div>failureCount: {result.failureCount}</div>
2787+
</div>
2788+
)
2789+
}
2790+
2791+
function App() {
2792+
const [show, toggle] = React.useReducer(x => !x, true)
2793+
2794+
return (
2795+
<div>
2796+
<button onClick={toggle}>{show ? 'hide' : 'show'}</button>
2797+
<button onClick={() => queryClient.cancelQueries({ queryKey: key })}>
2798+
cancel
2799+
</button>
2800+
{show && <Page />}
2801+
</div>
2802+
)
2803+
}
2804+
2805+
const rendered = renderWithClient(queryClient, <App />)
2806+
2807+
await waitFor(() => rendered.getByText('failureCount: 1'))
2808+
rendered.getByRole('button', { name: /hide/i }).click()
2809+
rendered.getByRole('button', { name: /cancel/i }).click()
2810+
rendered.getByRole('button', { name: /show/i }).click()
2811+
await waitFor(() => rendered.getByText('error: some error'))
2812+
2813+
// initial fetch (1), which will be cancelled, followed by new mount(2) + 2 retries = 4
2814+
expect(count).toBe(4)
2815+
2816+
consoleMock.mockRestore()
2817+
})
2818+
27142819
it('should always fetch if refetchOnMount is set to always', async () => {
27152820
const key = queryKey()
27162821
const states: UseQueryResult<string>[] = []

0 commit comments

Comments
 (0)