Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
node_modules
build
CHANGELOG.md

189 changes: 189 additions & 0 deletions src/react-query/hooks/__tests__/useQueryData.refetch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import {QueryClient, useInfiniteQuery, useQuery} from '@tanstack/react-query';
import {renderHook} from '@testing-library/react';

import {idle} from '../../../core';
import type {AnyInfiniteQueryDataSource} from '../../impl/infinite/types';
import type {AnyPlainQueryDataSource} from '../../impl/plain/types';
import {warnDisabledRefetch} from '../../utils/warnDisabledRefetch';
import {useQueryContext} from '../useQueryContext';
import {useQueryData} from '../useQueryData';

jest.mock('@tanstack/react-query', () => ({
...jest.requireActual('@tanstack/react-query'),
useQuery: jest.fn(),
useInfiniteQuery: jest.fn(),
}));

jest.mock('../useQueryContext');
jest.mock('../../utils/warnDisabledRefetch');

const mockUseQuery = useQuery as jest.MockedFunction<typeof useQuery>;
const mockUseInfiniteQuery = useInfiniteQuery as jest.MockedFunction<typeof useInfiniteQuery>;
const mockWarnDisabledRefetch = warnDisabledRefetch as jest.MockedFunction<
typeof warnDisabledRefetch
>;

describe('useQueryData refetch behavior', () => {
const mockQueryClient = new QueryClient();
const mockContext = {queryClient: mockQueryClient};

beforeEach(() => {
jest.clearAllMocks();
(useQueryContext as jest.Mock).mockReturnValue(mockContext);
});

const createMockQueryResult = (refetch = jest.fn()) => ({
data: 'test-data',
error: null,
status: 'success' as const,
fetchStatus: 'idle' as const,
isLoading: false,
isError: false,
isSuccess: true,
isPending: false,
refetch,
dataUpdatedAt: Date.now(),
errorUpdatedAt: 0,
failureCount: 0,
failureReason: null,
isFetched: true,
isFetchedAfterMount: true,
isFetching: false,
isInitialLoading: false,
isLoadingError: false,
isPaused: false,
isPlaceholderData: false,
isPreviousData: false,
isRefetchError: false,
isRefetching: false,
isStale: false,
promise: Promise.resolve('test-data'),
});

const createMockInfiniteResult = (refetch = jest.fn()) => ({
data: {pages: [['item1'], ['item2']], pageParams: [undefined, 'next-page']},
error: null,
status: 'success' as const,
fetchStatus: 'idle' as const,
isLoading: false,
isError: false,
isSuccess: true,
isPending: false,
refetch,
hasNextPage: false,
hasPreviousPage: false,
fetchNextPage: jest.fn(),
fetchPreviousPage: jest.fn(),
isFetchingNextPage: false,
isFetchingPreviousPage: false,
dataUpdatedAt: Date.now(),
errorUpdatedAt: 0,
failureCount: 0,
failureReason: null,
isFetched: true,
isFetchedAfterMount: true,
isFetching: false,
isInitialLoading: false,
isLoadingError: false,
isPaused: false,
isPlaceholderData: false,
isPreviousData: false,
isRefetchError: false,
isRefetching: false,
isStale: false,
promise: Promise.resolve({
pages: [['item1'], ['item2']],
pageParams: [undefined, 'next-page'],
}),
});

describe('plain data source', () => {
const plainDataSource: AnyPlainQueryDataSource = {
type: 'plain',
name: 'test-plain',
fetch: jest.fn().mockResolvedValue({data: 'test-data'}),
};

it('should wrap refetch when no enabled option', async () => {
const originalRefetch = jest.fn().mockResolvedValue({data: 'test', status: 'success'});
mockUseQuery.mockReturnValue(createMockQueryResult(originalRefetch) as any);

const {result} = renderHook(() => useQueryData(plainDataSource, {id: 1}));

expect(result.current.refetch).not.toBe(originalRefetch);
expect(result.current.refetch).not.toBe(mockWarnDisabledRefetch);

await result.current.refetch();
expect(originalRefetch).toHaveBeenCalledTimes(1);
});

it('should use warnDisabledRefetch when enabled: false', () => {
const originalRefetch = jest.fn();
mockUseQuery.mockReturnValue(createMockQueryResult(originalRefetch) as any);

const {result} = renderHook(() =>
useQueryData(plainDataSource, {id: 1}, {enabled: false}),
);

expect(result.current.refetch).toBe(mockWarnDisabledRefetch);
expect(result.current.refetch).not.toBe(originalRefetch);
});

it('should use warnDisabledRefetch when params is idle', () => {
const originalRefetch = jest.fn();
mockUseQuery.mockReturnValue(createMockQueryResult(originalRefetch) as any);

const {result} = renderHook(() => useQueryData(plainDataSource, idle));

expect(result.current.refetch).toBe(mockWarnDisabledRefetch);
expect(result.current.refetch).not.toBe(originalRefetch);
});
});

describe('infinite data source', () => {
const infiniteDataSource: AnyInfiniteQueryDataSource = {
type: 'infinite',
name: 'test-infinite',
fetch: jest.fn().mockResolvedValue({data: ['item1', 'item2']}),
next: jest.fn(),
};

it('should wrap refetch when no enabled option', async () => {
const originalRefetch = jest.fn().mockResolvedValue({
data: {pages: [], pageParams: []},
status: 'success',
});
mockUseInfiniteQuery.mockReturnValue(createMockInfiniteResult(originalRefetch) as any);

const {result} = renderHook(() => useQueryData(infiniteDataSource, {id: 1}));

expect(result.current.refetch).not.toBe(originalRefetch);
expect(result.current.refetch).not.toBe(mockWarnDisabledRefetch);

await result.current.refetch();
expect(originalRefetch).toHaveBeenCalledTimes(1);
});

it('should use warnDisabledRefetch when enabled: false', () => {
const originalRefetch = jest.fn();
mockUseInfiniteQuery.mockReturnValue(createMockInfiniteResult(originalRefetch) as any);

const {result} = renderHook(() =>
useQueryData(infiniteDataSource, {id: 1}, {enabled: false}),
);

expect(result.current.refetch).toBe(mockWarnDisabledRefetch);
expect(result.current.refetch).not.toBe(originalRefetch);
});

it('should use warnDisabledRefetch when params is idle', () => {
const originalRefetch = jest.fn();
mockUseInfiniteQuery.mockReturnValue(createMockInfiniteResult(originalRefetch) as any);

const {result} = renderHook(() => useQueryData(infiniteDataSource, idle));

expect(result.current.refetch).toBe(mockWarnDisabledRefetch);
expect(result.current.refetch).not.toBe(originalRefetch);
});
});
});
7 changes: 6 additions & 1 deletion src/react-query/impl/infinite/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useMemo} from 'react';

import {useInfiniteQuery} from '@tanstack/react-query';
import {skipToken, useInfiniteQuery} from '@tanstack/react-query';
import type {InfiniteData, InfiniteQueryObserverOptions} from '@tanstack/react-query';

import type {
Expand All @@ -16,6 +16,8 @@ import type {
} from '../../../core';
import {useRefetchInterval} from '../../hooks/useRefetchInterval';
import {normalizeStatus} from '../../utils/normalizeStatus';
import {warnDisabledRefetch} from '../../utils/warnDisabledRefetch';
import {wrapRefetch} from '../../utils/wrapRefetch';

import type {AnyInfiniteQueryDataSource, InfiniteQueryObserverExtendedOptions} from './types';
import {composeOptions} from './utils';
Expand Down Expand Up @@ -63,11 +65,14 @@ export const useInfiniteQueryData = <TDataSource extends AnyInfiniteQueryDataSou
[state.data],
);

const isDisabled = composedOptions.enabled === false || composedOptions.queryFn === skipToken;

return {
...state,
status: normalizeStatus(state.status, state.fetchStatus),
data: transformedData,
originalStatus: state.status,
originalData: state.data,
refetch: isDisabled ? warnDisabledRefetch : wrapRefetch(state.refetch),
} as DataSourceState<TDataSource>;
};
1 change: 1 addition & 0 deletions src/react-query/impl/infinite/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ type ResultWrapper<TResult, TRequest, TResponse, TData, TError> =
{
status: DataLoaderStatus;
data: Array<FlatArray<Array<ActualData<TData, TResponse>>, 1>>;
refetch: () => Promise<void>;
}
> & {
originalStatus: TResult['status'];
Expand Down
7 changes: 6 additions & 1 deletion src/react-query/impl/plain/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {type QueryObserverOptions, useQuery} from '@tanstack/react-query';
import {type QueryObserverOptions, skipToken, useQuery} from '@tanstack/react-query';

import type {
DataSourceContext,
Expand All @@ -12,6 +12,8 @@ import type {
} from '../../../core';
import {useRefetchInterval} from '../../hooks/useRefetchInterval';
import {normalizeStatus} from '../../utils/normalizeStatus';
import {warnDisabledRefetch} from '../../utils/warnDisabledRefetch';
import {wrapRefetch} from '../../utils/wrapRefetch';

import type {AnyPlainQueryDataSource, QueryObserverExtendedOptions} from './types';
import {composeOptions} from './utils';
Expand Down Expand Up @@ -52,9 +54,12 @@ export const usePlainQueryData = <TDataSource extends AnyPlainQueryDataSource>(
const composedOptions = usePlainQueryDataOptions(extendedOptions);
const state = useQuery(composedOptions);

const isDisabled = composedOptions.enabled === false || composedOptions.queryFn === skipToken;

return {
...state,
status: normalizeStatus(state.status, state.fetchStatus),
originalStatus: state.status,
refetch: isDisabled ? warnDisabledRefetch : wrapRefetch(state.refetch),
} as DataSourceState<TDataSource>;
};
8 changes: 7 additions & 1 deletion src/react-query/impl/plain/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,11 @@ export type AnyPlainQueryDataSource = PlainQueryDataSource<any, any, any, any, a

type ResultWrapper<TResult, TResponse, TData, TError> =
TResult extends QueryObserverResult<ActualData<TData, TResponse>, TError>
? Overwrite<TResult, {status: DataLoaderStatus}> & {originalStatus: TResult['status']}
? Overwrite<
TResult,
{
status: DataLoaderStatus;
refetch: () => Promise<void>;
}
> & {originalStatus: TResult['status']}
: never;
5 changes: 5 additions & 0 deletions src/react-query/types/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export interface QueryDataAdditionalOptions<
TQueryKey extends QueryKey = QueryKey,
> {
refetchInterval?: RefetchInterval<TQueryFnData, TError, TQueryData, TQueryKey>;
/**
* @deprecated The use of the enabled option is deprecated.
* It is recommended to use idle as query parameters to control query state.
*/
enabled?: boolean;
/** Normalization configuration (enable/disable) */
normalize?: boolean;
/** Optimistic data update configuration */
Expand Down
8 changes: 8 additions & 0 deletions src/react-query/utils/warn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function warn(msg: string) {
if (!msg || process.env.NODE_ENV === 'production') {
return;
}

// eslint-disable-next-line no-console
console.warn(msg);
}
5 changes: 5 additions & 0 deletions src/react-query/utils/warnDisabledRefetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {warn} from './warn';

export const warnDisabledRefetch = async (): Promise<void> => {
warn('Disabled refetch is called');
};
9 changes: 9 additions & 0 deletions src/react-query/utils/wrapRefetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type {QueryObserverResult, RefetchOptions} from '@tanstack/react-query';

export function wrapRefetch<TData, TError>(
refetch: (options?: RefetchOptions) => Promise<QueryObserverResult<TData, TError>>,
): (options?: RefetchOptions) => Promise<void> {
return async (options?: RefetchOptions) => {
await refetch(options);
};
}
Loading