diff --git a/examples/playground/src/index.js b/examples/playground/src/index.js index ee96cf5f95..475e1ca0c7 100644 --- a/examples/playground/src/index.js +++ b/examples/playground/src/index.js @@ -397,7 +397,7 @@ function fetchTodoById({ id }) { new Error(JSON.stringify({ fetchTodoById: { id } }, null, 2)) ); } - resolve(list.find((d) => d.id == id)); + resolve(list.find((d) => d.id === id)); }, queryTimeMin + Math.random() * (queryTimeMax - queryTimeMin)); }); } diff --git a/src/core/query.ts b/src/core/query.ts index f82b0281f6..2627c43184 100644 --- a/src/core/query.ts +++ b/src/core/query.ts @@ -572,7 +572,10 @@ export class Query< fetchStatus: canFetch(this.options.networkMode) ? 'fetching' : 'paused', - status: !state.dataUpdatedAt ? 'loading' : state.status, + ...(!state.dataUpdatedAt && { + error: null, + status: 'loading', + }), } case 'success': return { diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts index c2a249a9aa..587be13fb2 100644 --- a/src/core/queryObserver.ts +++ b/src/core/queryObserver.ts @@ -69,7 +69,10 @@ export class QueryObserver< > private previousQueryResult?: QueryObserverResult private previousSelectError: TError | null - private previousSelectFn?: (data: TQueryData) => TData + private previousSelect?: { + fn: (data: TQueryData) => TData + result: TData + } private staleTimeoutId?: ReturnType private refetchIntervalId?: ReturnType private currentRefetchInterval?: number | false @@ -473,14 +476,17 @@ export class QueryObserver< if ( prevResult && state.data === prevResultState?.data && - options.select === this.previousSelectFn && + options.select === this.previousSelect?.fn && !this.previousSelectError ) { - data = prevResult.data + data = this.previousSelect.result } else { try { - this.previousSelectFn = options.select data = options.select(state.data) + this.previousSelect = { + fn: options.select, + result: data, + } if (options.structuralSharing !== false) { data = replaceEqualDeep(prevResult?.data, data) } diff --git a/src/devtools/styledComponents.ts b/src/devtools/styledComponents.ts index 18968629c5..0f41715025 100644 --- a/src/devtools/styledComponents.ts +++ b/src/devtools/styledComponents.ts @@ -65,6 +65,7 @@ export const QueryKey = styled('span', { export const Code = styled('code', { fontSize: '.9em', + color: 'inherit', }) export const Input = styled('input', (_props, theme) => ({ diff --git a/src/reactjs/tests/useQuery.test.tsx b/src/reactjs/tests/useQuery.test.tsx index 193dc892b1..44d0ac4caf 100644 --- a/src/reactjs/tests/useQuery.test.tsx +++ b/src/reactjs/tests/useQuery.test.tsx @@ -4102,6 +4102,56 @@ describe('useQuery', () => { expect(selectRun).toBe(3) }) + it('select should always return the correct state', async () => { + const key1 = queryKey() + + function Page() { + const [count, inc] = React.useReducer(prev => prev + 1, 2) + const [forceValue, forceUpdate] = React.useReducer(prev => prev + 1, 1) + + const state = useQuery( + key1, + async () => { + await sleep(10) + return 0 + }, + { + select: React.useCallback( + (data: number) => { + return `selected ${data + count}` + }, + [count] + ), + placeholderData: 99, + } + ) + + return ( +
+

Data: {state.data}

+

forceValue: {forceValue}

+ + +
+ ) + } + + const rendered = renderWithClient(queryClient, ) + await waitFor(() => rendered.getByText('Data: selected 101')) // 99 + 2 + + await waitFor(() => rendered.getByText('Data: selected 2')) // 0 + 2 + + rendered.getByRole('button', { name: /inc/i }).click() + + await waitFor(() => rendered.getByText('Data: selected 3')) // 0 + 3 + + rendered.getByRole('button', { name: /forceUpdate/i }).click() + + await waitFor(() => rendered.getByText('forceValue: 2')) + // data should still be 3 after an independent re-render + await waitFor(() => rendered.getByText('Data: selected 3')) + }) + it('should cancel the query function when there are no more subscriptions', async () => { const key = queryKey() let cancelFn: jest.Mock = jest.fn() @@ -4611,6 +4661,82 @@ describe('useQuery', () => { consoleMock.mockRestore() }) + it('should have no error in loading state when refetching after error occurred', async () => { + const consoleMock = mockConsoleError() + const key = queryKey() + const states: UseQueryResult[] = [] + const error = new Error('oops') + + let count = 0 + + function Page() { + const state = useQuery( + key, + async () => { + await sleep(10) + if (count === 0) { + count++ + throw error + } + return 5 + }, + { + retry: false, + } + ) + + states.push(state) + + if (state.isLoading) { + return
status: loading
+ } + if (state.error instanceof Error) { + return ( +
+
error
+ +
+ ) + } + return
data: {state.data}
+ } + + const rendered = renderWithClient(queryClient, ) + + await waitFor(() => rendered.getByText('error')) + + fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) + await waitFor(() => rendered.getByText('data: 5')) + + await waitFor(() => expect(states.length).toBe(4)) + + expect(states[0]).toMatchObject({ + status: 'loading', + data: undefined, + error: null, + }) + + expect(states[1]).toMatchObject({ + status: 'error', + data: undefined, + error, + }) + + expect(states[2]).toMatchObject({ + status: 'loading', + data: undefined, + error: null, + }) + + expect(states[3]).toMatchObject({ + status: 'success', + data: 5, + error: null, + }) + + consoleMock.mockRestore() + }) + describe('networkMode online', () => { it('online queries should not start fetching if you are offline', async () => { const onlineMock = mockNavigatorOnLine(false) diff --git a/src/reactjs/types.ts b/src/reactjs/types.ts index 3d2079d110..13ac8c4510 100644 --- a/src/reactjs/types.ts +++ b/src/reactjs/types.ts @@ -1,16 +1,12 @@ -import { RetryValue, RetryDelayValue } from '../core/retryer' import { InfiniteQueryObserverOptions, InfiniteQueryObserverResult, MutationObserverResult, - MutationKey, QueryObserverOptions, QueryObserverResult, QueryKey, - MutationFunction, - MutateOptions, - MutationMeta, - NetworkMode, + MutationObserverOptions, + MutateFunction, } from '../core/types' export interface UseBaseQueryOptions< @@ -74,35 +70,10 @@ export interface UseMutationOptions< TError = unknown, TVariables = void, TContext = unknown -> { - mutationFn?: MutationFunction - mutationKey?: MutationKey - cacheTime?: number - onMutate?: ( - variables: TVariables - ) => Promise | TContext | undefined - onSuccess?: ( - data: TData, - variables: TVariables, - context: TContext | undefined - ) => Promise | void - onError?: ( - error: TError, - variables: TVariables, - context: TContext | undefined - ) => Promise | void - onSettled?: ( - data: TData | undefined, - error: TError | null, - variables: TVariables, - context: TContext | undefined - ) => Promise | void - retry?: RetryValue - retryDelay?: RetryDelayValue - networkMode?: NetworkMode - useErrorBoundary?: boolean | ((error: TError) => boolean) - meta?: MutationMeta -} +> extends Omit< + MutationObserverOptions, + '_defaulted' | 'variables' + > {} export type UseMutateFunction< TData = unknown, @@ -110,8 +81,7 @@ export type UseMutateFunction< TVariables = void, TContext = unknown > = ( - variables: TVariables, - options?: MutateOptions + ...args: Parameters> ) => void export type UseMutateAsyncFunction< @@ -119,10 +89,7 @@ export type UseMutateAsyncFunction< TError = unknown, TVariables = void, TContext = unknown -> = ( - variables: TVariables, - options?: MutateOptions -) => Promise +> = MutateFunction export type UseBaseMutationResult< TData = unknown,