diff --git a/src/__tests__/useFetch.test.tsx b/src/__tests__/useFetch.test.tsx index 6ad2121..54f836a 100644 --- a/src/__tests__/useFetch.test.tsx +++ b/src/__tests__/useFetch.test.tsx @@ -1320,3 +1320,48 @@ describe('useFetch - BROWSER - persistence', (): void => { expect(result.error.message).toBe('You cannot use option \'persist\' with cachePolicy: network-only 🙅‍♂️') }) }) + + +describe('useFetch - racing conditions', (): void => { + afterEach((): void => { + cleanup() + fetch.resetMocks() + }) + + it('should fail to process a text response with responseType: `json`', async (): Promise => { + cleanup() + fetch.resetMocks() + + let finisDdelayedResponse = () => {}; + const delayedResponse = new Promise((_resolve) => { + finisDdelayedResponse = () => {_resolve(JSON.stringify("first"))}; + }); + + fetch.mockResponseOnce(() => delayedResponse) + const {rerender, result, waitForNextUpdate} = renderHook( + (props:{page:number}) => useFetch('a-fake-url-'+props.page, { data: '', responseType: 'json' }, [props.page]), + {initialProps: {page:1}} + ) + + // Load page 1 (but it doesn't resolve yet) + expect(result.current.loading).toBe(true) + expect(result.current.data).toBe(""); + + // Change to page 2 and let it resolve directly + fetch.mockResponseOnce(JSON.stringify("second")) + act(() => rerender({ page: 2 })) + + expect(result.current.loading).toBe(true) + expect(result.current.data).toBe(""); + await waitForNextUpdate() + expect(result.current.loading).toBe(false) + expect(result.current.data).toBe("second"); + + // Now the first delayed page is finished. + // The newest data should not be taken into account, as it is an old request + // That is only now finished. + act(() => finisDdelayedResponse()); + expect(result.current.loading).toBe(false) + expect(result.current.data).toBe("second"); + }) +}) diff --git a/src/useFetch.ts b/src/useFetch.ts index f51f921..00c0060 100644 --- a/src/useFetch.ts +++ b/src/useFetch.ts @@ -103,18 +103,24 @@ function useFetch(...args: UseFetchArgs): UseFetch { try { if (response.isCached && cachePolicy === CACHE_FIRST) { - newRes = response.cached as Response + newRes = response.cached?.clone() as Res } else { - newRes = (await fetch(url, options)).clone() + newRes = (await fetch(url, options)).clone() as Res + } + + if(theController == controller.current) { + res.current = newRes.clone() } - res.current = newRes.clone() newData = await tryGetData(newRes, defaults.data, responseType) - res.current.data = onNewData(data.current, newData) + newRes.data = onNewData(data.current, newData) - res.current = interceptors.response ? await interceptors.response({ response: res.current, request: requestInit }) : res.current - invariant('data' in res.current, 'You must have `data` field on the Response returned from your `interceptors.response`') - data.current = res.current.data as TData + newRes = interceptors.response ? await interceptors.response({ response: newRes, request: requestInit }) : newRes + invariant('data' in newRes, 'You must have `data` field on the Response returned from your `interceptors.response`') + if(theController == controller.current) { + res.current = newRes + data.current = res.current.data as TData + } const opts = { attempt: attempt.current, response: newRes } const shouldRetry = ( @@ -137,7 +143,7 @@ function useFetch(...args: UseFetchArgs): UseFetch { if (Array.isArray(data.current) && !!(data.current.length % perPage)) hasMore.current = false } catch (err) { - if (attempt.current >= retries && timedout.current) error.current = makeError('AbortError', 'Timeout Error') + if (attempt.current >= retries && timedout.current && theController == controller.current) error.current = makeError('AbortError', 'Timeout Error') const opts = { attempt: attempt.current, error: err } const shouldRetry = ( // if we just have `retries` set with NO `retryOn` then @@ -151,20 +157,23 @@ function useFetch(...args: UseFetchArgs): UseFetch { const theData = await retry(opts, routeOrBody, body) return theData } - if (err.name !== 'AbortError') { + if (err.name !== 'AbortError' && theController == controller.current) { error.current = err } } finally { - timedout.current = false if (timer) clearTimeout(timer) - controller.current = undefined } - if (newRes && !newRes.ok && !error.current) error.current = makeError(newRes.status, newRes.statusText) - if (!suspense) setLoading(false) - if (attempt.current === retries) attempt.current = 0 - if (error.current) onError({ error: error.current }) + if(theController == controller.current) { + if (newRes && !newRes.ok && !error.current) error.current = makeError(newRes.status, newRes.statusText) + if (!suspense) setLoading(false) + if (attempt.current === retries) attempt.current = 0 + if (error.current) onError({ error: error.current }) + + timedout.current = false + controller.current = undefined + } return data.current } // end of doFetch()