From 02248d01196141cd3d243d0ce8138e67e78cf17a Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sat, 23 Nov 2024 18:30:03 -0500 Subject: [PATCH] Keep `isSuccess: true` when switching to an uninitialized cache entry --- .../toolkit/src/query/react/buildHooks.ts | 10 ++- .../src/query/tests/buildHooks.test.tsx | 64 +++++++++++++++++-- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index 5f23d7a4f7..2eebe2e7a3 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -927,13 +927,19 @@ export function buildHooks({ // isFetching = true any time a request is in flight const isFetching = currentState.isLoading + // isLoading = true only when loading while no data is present yet (initial load with no data in the cache) const isLoading = (!lastResult || lastResult.isLoading || lastResult.isUninitialized) && !hasData && isFetching - // isSuccess = true when data is present - const isSuccess = currentState.isSuccess || (isFetching && hasData) + + // isSuccess = true when data is present. + // That includes cases where the _current_ item is either actively + // fetching or about to fetch due to an uninitialized entry. + const isSuccess = + currentState.isSuccess || + ((isFetching || currentState.isUninitialized) && hasData) return { ...currentState, diff --git a/packages/toolkit/src/query/tests/buildHooks.test.tsx b/packages/toolkit/src/query/tests/buildHooks.test.tsx index 36ffbe3239..845f08f568 100644 --- a/packages/toolkit/src/query/tests/buildHooks.test.tsx +++ b/packages/toolkit/src/query/tests/buildHooks.test.tsx @@ -126,10 +126,10 @@ const api = createApi({ }, }), queryWithDeepArg: build.query({ - query: ({ param: { nested }}) => nested, + query: ({ param: { nested } }) => nested, serializeQueryArgs: ({ queryArgs }) => { return queryArgs.param.nested - } + }, }), }), }) @@ -383,6 +383,54 @@ describe('hooks tests', () => { expect(fetchingHist).toEqual([true, false, true, false]) }) + test('`isSuccess` does not jump back false on subsequent queries', async () => { + type LoadingState = { + id: number + isFetching: boolean + isSuccess: boolean + } + const loadingHistory: LoadingState[] = [] + + function User({ id }: { id: number }) { + const queryRes = api.endpoints.getUser.useQuery(id) + + useEffect(() => { + const { isFetching, isSuccess } = queryRes + loadingHistory.push({ id, isFetching, isSuccess }) + }, [id, queryRes]) + return ( +
+ {queryRes.status === QueryStatus.fulfilled && id} +
+ ) + } + + let { rerender } = render(, { wrapper: storeRef.wrapper }) + + await waitFor(() => + expect(screen.getByTestId('status').textContent).toBe('1'), + ) + rerender() + + await waitFor(() => + expect(screen.getByTestId('status').textContent).toBe('2'), + ) + + expect(loadingHistory).toEqual([ + // Initial render(s) + { id: 1, isFetching: true, isSuccess: false }, + { id: 1, isFetching: true, isSuccess: false }, + // Data returned + { id: 1, isFetching: false, isSuccess: true }, + // ID changed, there's an uninitialized cache entry. + // IMPORTANT: `isSuccess` should not be false here. + // We have valid data already for the old item. + { id: 2, isFetching: true, isSuccess: true }, + { id: 2, isFetching: true, isSuccess: true }, + { id: 2, isFetching: false, isSuccess: true }, + ]) + }) + test('useQuery hook respects refetchOnMountOrArgChange: true', async () => { let data, isLoading, isFetching function User() { @@ -674,9 +722,11 @@ describe('hooks tests', () => { }) test(`useQuery shouldn't call args serialization if request skipped`, async () => { - expect(() => renderHook(() => api.endpoints.queryWithDeepArg.useQuery(skipToken), { - wrapper: storeRef.wrapper, - })).not.toThrow() + expect(() => + renderHook(() => api.endpoints.queryWithDeepArg.useQuery(skipToken), { + wrapper: storeRef.wrapper, + }), + ).not.toThrow() }) test(`useQuery gracefully handles bigint types`, async () => { @@ -2861,6 +2911,7 @@ describe('skip behavior', () => { }) expect(result.current).toEqual({ ...uninitialized, + isSuccess: true, currentData: undefined, data: { name: 'Timmy' }, }) @@ -2898,6 +2949,7 @@ describe('skip behavior', () => { }) expect(result.current).toEqual({ ...uninitialized, + isSuccess: true, currentData: undefined, data: { name: 'Timmy' }, }) @@ -2936,7 +2988,7 @@ describe('skip behavior', () => { // even though it's skipped. `currentData` is undefined, since that matches the current arg. expect(result.current).toMatchObject({ status: QueryStatus.uninitialized, - isSuccess: false, + isSuccess: true, data: { name: 'Timmy' }, currentData: undefined, })