diff --git a/README.md b/README.md index 65131c3430..0b0170b7cc 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,6 @@ A big thanks to both [Draqula](https://github.com/vadimdemedes/draqula) for insp [Zeit's SWR](https://github.com/zeit/swr) is a great library, and is very similar in spirit and implementation to React Query with a few notable differences: - Automatic Cache Garbage Collection - React Query handles automatic cache purging for inactive queries and garbage collection. This can mean a much smaller memory footprint for apps that consume a lot of data or data that is changing often in a single session -- No Default Data Fetcher Function - React Query does not ship with a default fetcher (but can easily be wrapped inside of a custom hook to achieve the same functionality) - `useMutation` - A dedicated hook for handling generic lifecycles around triggering mutations and handling their side-effects in applications. SWR does not ship with anything similar, and you may find yourself reimplementing most if not all of `useMutation`'s functionality in user-land. With this hook, you can extend the lifecycle of your mutations to reliably handle successful refetching strategies, failure rollbacks and error handling. - Prefetching - React Query ships with 1st class prefetching utilities which not only come in handy with non-suspenseful apps but also make fetch-as-you-render patterns possible with React Query. SWR does not come with similar utilities and relies on `` and/or manually fetching and updating the query cache - Query cancellation integration is baked into React Query. You can easily use this to wire up request cancellation in most popular fetching libraries, including but not limited to fetch and axios. @@ -164,8 +163,8 @@ This library is being built and maintained by me, @tannerlinsley and I am always - - + + @@ -178,8 +177,8 @@ This library is being built and maintained by me, @tannerlinsley and I am always - - + + @@ -485,7 +484,7 @@ To do this, you can use the following 2 approaches: ### Pass a falsy query key -If a query isn't ready to be requested yet, just pass a falsy value as the query key or as an item in the query key: +If a query isn't ready to be requested yet, just pass a falsy value as the query key: ```js // Get the user @@ -828,7 +827,7 @@ const { status, data, error } = useQuery('todos', fetchTodoList, { ## Prefetching -If you're lucky enough, you may know enough about what your users will do to be able to prefetch the data they need before it's needed! If this is the case, then you're in luck. You can either use the `prefetchQuery` function to prefetch the results of a query to be placed into the cache: +If you're lucky enough, you may know enough about what your users will do to be able to prefetch the data they need before it's needed! If this is the case, you can use the `prefetchQuery` function to prefetch the results of a query to be placed into the cache: ```js import { queryCache } from 'react-query' @@ -843,7 +842,7 @@ const prefetchTodos = async () => { The next time a `useQuery` instance is used for a prefetched query, it will use the cached data! If no instances of `useQuery` appear for a prefetched query, it will be deleted and garbage collected after the time specified in `cacheTime`. -Alternatively, if you already have the data for your query synchronously available, you can use the [Query Cache's `setQueryData` method](#querycachesetquerydata) to directly add or update a query's cached result +Alternatively, if you already have the data for your query synchronously available, you can use the [Query Cache's `setQueryData` method](#querycachesetquerydata) to directly add or update a query's cached result. ## Initial Data @@ -1102,7 +1101,7 @@ const CreateTodo = () => { } ``` -Even with just variables, mutations aren't all that special, but when used with the `onSuccess` option, the [Query Cache's `refetchQueries` method](#querycacherefetchqueries) method and the [Query Cache's `setQueryData` method](#querycachesetquerydata), mutations become a very powerful tool. +Even with just variables, mutations aren't all that special, but when used with the `onSuccess` option, the [Query Cache's `refetchQueries` method](#querycacherefetchqueries) and the [Query Cache's `setQueryData` method](#querycachesetquerydata), mutations become a very powerful tool. Note that since version 1.1.0, the `mutate` function is no longer called synchronously so you cannot use it in an event callback. If you need to access the event in `onSubmit` you need to wrap `mutate` in another function. This is due to [React event pooling](https://reactjs.org/docs/events.html#event-pooling). @@ -1992,7 +1991,7 @@ const { - `loading` if the query is in an initial loading state. This means there is no cached data and the query is currently fetching, eg `isFetching === true`) - `error` if the query attempt resulted in an error. The corresponding `error` property has the error received from the attempted fetch - `success` if the query has received a response with no errors and is ready to display its data. The corresponding `data` property on the query is the data received from the successful fetch or if the query is in `manual` mode and has not been fetched yet `data` is the first `initialData` supplied to the query on initialization. -- `resolveData: Any` +- `resolvedData: Any` - Defaults to `undefined`. - The last successfully resolved data for the query. - When fetching based on a new query key, the value will resolve to the last known successful value, regardless of query key @@ -2028,6 +2027,8 @@ const { isFetching, failureCount, refetch, + fetchMore, + canFetchMore, } = useInfiniteQuery(queryKey, [, queryVariables], queryFn, { getFetchMore: (lastPage, allPages) => fetchMoreVariable manual, diff --git a/src/config.js b/src/config.js index f309ac4afd..f00bfe25b8 100644 --- a/src/config.js +++ b/src/config.js @@ -3,26 +3,28 @@ import { noop, stableStringify, identity, deepEqual } from './utils' export const configContext = React.createContext() +const DEFAULTS = { + retry: 3, + retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), + staleTime: 0, + cacheTime: 5 * 60 * 1000, + refetchAllOnWindowFocus: true, + refetchInterval: false, + suspense: false, + queryKeySerializerFn: defaultQueryKeySerializerFn, + queryFnParamsFilter: identity, + throwOnError: false, + useErrorBoundary: undefined, // this will default to the suspense value + onMutate: noop, + onSuccess: noop, + onError: noop, + onSettled: noop, + refetchOnMount: true, + isDataEqual: deepEqual, +} + export const defaultConfigRef = { - current: { - retry: 3, - retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), - staleTime: 0, - cacheTime: 5 * 60 * 1000, - refetchAllOnWindowFocus: true, - refetchInterval: false, - suspense: false, - queryKeySerializerFn: defaultQueryKeySerializerFn, - queryFnParamsFilter: identity, - throwOnError: false, - useErrorBoundary: undefined, // this will default to the suspense value - onMutate: noop, - onSuccess: noop, - onError: noop, - onSettled: noop, - refetchOnMount: true, - isDataEqual: deepEqual, - }, + current: DEFAULTS, } export function useConfigContext() { @@ -46,6 +48,19 @@ export function ReactQueryConfigProvider({ config, children }) { return newConfig }, [config, configContextValue]) + React.useEffect(() => { + // restore previous config on unmount + return () => { + defaultConfigRef.current = { ...(configContextValue || DEFAULTS) } + + // Default useErrorBoundary to the suspense value + if (typeof defaultConfigRef.current.useErrorBoundary === 'undefined') { + defaultConfigRef.current.useErrorBoundary = + defaultConfigRef.current.suspense + } + } + }, [configContextValue]) + if (!configContextValue) { defaultConfigRef.current = newConfig } diff --git a/src/queryCache.js b/src/queryCache.js index 79a7467957..4dc47f72cf 100644 --- a/src/queryCache.js +++ b/src/queryCache.js @@ -84,6 +84,7 @@ export function makeQueryCache() { } cache.clear = () => { + Object.values(cache.queries).forEach(query => query.clear()) cache.queries = {} notifyGlobalListeners() } @@ -516,6 +517,12 @@ export function makeQueryCache() { query.scheduleStaleTimeout() } + query.clear = () => { + clearTimeout(query.staleTimeout) + clearTimeout(query.cacheTimeout) + query.cancel() + } + return query } diff --git a/src/tests/config.test.js b/src/tests/config.test.js index 6eb76930f9..bc646fc94f 100644 --- a/src/tests/config.test.js +++ b/src/tests/config.test.js @@ -1,8 +1,15 @@ -import React from 'react' -import { render, waitForElement, cleanup } from '@testing-library/react' +import React, { useState } from 'react' +import { + act, + fireEvent, + render, + waitForElement, + cleanup, +} from '@testing-library/react' import { ReactQueryConfigProvider, useQuery, + queryCache, ReactQueryCacheProvider, } from '../index' @@ -10,7 +17,7 @@ import { sleep } from './utils' describe('config', () => { afterEach(() => { - cleanup() + queryCache.clear() }) // See https://github.com/tannerlinsley/react-query/issues/105 @@ -46,4 +53,147 @@ describe('config', () => { expect(onSuccess).toHaveBeenCalledWith('data') }) + + it('should reset to defaults when all providers are unmounted', async () => { + const onSuccess = jest.fn() + + const config = { + refetchAllOnWindowFocus: false, + refetchOnMount: false, + retry: false, + manual: true, + } + + const queryFn = async () => { + await sleep(10) + return 'data' + } + + function Page() { + const { data } = useQuery('test', queryFn) + + return ( +
+

Data: {data || 'none'}

+
+ ) + } + + const rendered = render( + + + + ) + + await rendered.findByText('Data: none') + + act(() => { + queryCache.prefetchQuery('test', queryFn, { force: true }) + }) + + await rendered.findByText('Data: data') + + // tear down and unmount + cleanup() + + // wipe query cache/stored config + act(() => queryCache.clear()) + onSuccess.mockClear() + + // rerender WITHOUT config provider, + // so we are NOT passing the config above (refetchOnMount should be `true` by default) + const rerendered = render() + + await rerendered.findByText('Data: data') + }) + + it('should reset to previous config when nested provider is unmounted', async () => { + let counterRef = 0 + const parentOnSuccess = jest.fn() + + const parentConfig = { + refetchOnMount: false, + onSuccess: parentOnSuccess, + } + + const childConfig = { + refetchOnMount: true, + + // Override onSuccess of parent, making it a no-op + onSuccess: undefined, + } + + const queryFn = async () => { + await sleep(10) + counterRef += 1 + return String(counterRef) + } + + function Component() { + const { data, refetch } = useQuery('test', queryFn) + + return ( +
+

Data: {data}

+ +
+ ) + } + + function Page() { + const [childConfigEnabled, setChildConfigEnabled] = useState(true) + + return ( +
+ {childConfigEnabled && ( + + + + )} + {!childConfigEnabled && } + +
+ ) + } + + const rendered = render( + + + + + + ) + + await rendered.findByText('Data: 1') + + expect(parentOnSuccess).not.toHaveBeenCalled() + + fireEvent.click(rendered.getByTestId('refetch')) + + await rendered.findByText('Data: 2') + + expect(parentOnSuccess).not.toHaveBeenCalled() + + parentOnSuccess.mockReset() + + fireEvent.click(rendered.getByTestId('disableChildConfig')) + + await rendered.findByText('Data: 2') + + // it should not refetch on mount + expect(parentOnSuccess).not.toHaveBeenCalled() + + fireEvent.click(rendered.getByTestId('refetch')) + + await rendered.findByText('Data: 3') + + expect(parentOnSuccess).toHaveBeenCalledTimes(1) + }) }) diff --git a/src/tests/suspense.test.js b/src/tests/suspense.test.js index e3904a10fb..c7f95f899f 100644 --- a/src/tests/suspense.test.js +++ b/src/tests/suspense.test.js @@ -1,4 +1,9 @@ -import { render, waitForElement, fireEvent, cleanup } from '@testing-library/react' +import { + render, + waitForElement, + fireEvent, + cleanup, +} from '@testing-library/react' import * as React from 'react' import { useQuery, ReactQueryCacheProvider, queryCache } from '../index' diff --git a/src/tests/usePaginatedQuery.test.js b/src/tests/usePaginatedQuery.test.js index 91afda3f57..422f9e2194 100644 --- a/src/tests/usePaginatedQuery.test.js +++ b/src/tests/usePaginatedQuery.test.js @@ -192,4 +192,39 @@ describe('usePaginatedQuery', () => { rendered.getByText('Data second-search 1') await waitForElement(() => rendered.getByText('Data second-search 2')) }) + + it('should not suspend while fetching the next page', async () => { + function Page() { + const [page, setPage] = React.useState(1) + + const { resolvedData } = usePaginatedQuery( + ['data', { page }], + async (queryName, { page }) => { + await sleep(1) + return page + }, + { + initialData: 0, + suspense: true, + } + ) + + return ( +
+

Data {resolvedData}

+ +
+ ) + } + + // render will throw if Page is suspended + const rendered = render( + + + + ) + + fireEvent.click(rendered.getByText('next')) + await waitForElement(() => rendered.getByText('Data 2')) + }) }) diff --git a/src/tests/useQuery.test.js b/src/tests/useQuery.test.js index 659980ce0e..84c20e5130 100644 --- a/src/tests/useQuery.test.js +++ b/src/tests/useQuery.test.js @@ -1,4 +1,11 @@ -import { render, act, waitForElement, fireEvent, cleanup } from '@testing-library/react' +import { + render, + act, + waitForElement, + fireEvent, + cleanup, + waitForDomChange, +} from '@testing-library/react' import { ErrorBoundary } from 'react-error-boundary' import * as React from 'react' @@ -998,4 +1005,70 @@ describe('useQuery', () => { await waitForElement(() => rendered.getByText('rendered')) }) + + it('should update data upon interval changes', async () => { + let count = 0 + function Page() { + const [int, setInt] = React.useState(200) + const { data } = useQuery('/api', () => count++, { + refetchInterval: int, + }) + return ( +
setInt(num => (num < 400 ? num + 100 : 0))}> + count: {data} +
+ ) + } + const { container } = render() + expect(container.firstChild.textContent).toEqual('count: ') + await waitForDomChange({ container }) // mount + expect(container.firstChild.textContent).toEqual('count: 0') + await act(() => { + return new Promise(res => setTimeout(res, 210)) + }) + expect(container.firstChild.textContent).toEqual('count: 1') + await act(() => { + return new Promise(res => setTimeout(res, 50)) + }) + expect(container.firstChild.textContent).toEqual('count: 1') + await act(() => { + return new Promise(res => setTimeout(res, 150)) + }) + expect(container.firstChild.textContent).toEqual('count: 2') + await act(() => { + fireEvent.click(container.firstElementChild) + // it will clear 200ms timer and setup a new 300ms timer + return new Promise(res => setTimeout(res, 200)) + }) + expect(container.firstChild.textContent).toEqual('count: 2') + await act(() => { + return new Promise(res => setTimeout(res, 110)) + }) + expect(container.firstChild.textContent).toEqual('count: 3') + await act(() => { + // wait for new 300ms timer + return new Promise(res => setTimeout(res, 310)) + }) + expect(container.firstChild.textContent).toEqual('count: 4') + await act(() => { + fireEvent.click(container.firstElementChild) + // it will clear 300ms timer and setup a new 400ms timer + return new Promise(res => setTimeout(res, 300)) + }) + expect(container.firstChild.textContent).toEqual('count: 4') + await act(() => { + return new Promise(res => setTimeout(res, 110)) + }) + expect(container.firstChild.textContent).toEqual('count: 5') + await act(() => { + fireEvent.click(container.firstElementChild) + // it will clear 400ms timer and stop + return new Promise(res => setTimeout(res, 110)) + }) + expect(container.firstChild.textContent).toEqual('count: 5') + await act(() => { + return new Promise(res => setTimeout(res, 110)) + }) + expect(container.firstChild.textContent).toEqual('count: 5') + }) }) diff --git a/src/usePaginatedQuery.js b/src/usePaginatedQuery.js index 52960a8a3f..6dccecb54b 100644 --- a/src/usePaginatedQuery.js +++ b/src/usePaginatedQuery.js @@ -39,12 +39,14 @@ export function usePaginatedQuery(...args) { status = 'success' } - handleSuspense(query) - - return { + const paginatedQuery = { ...query, resolvedData, latestData, status, } + + handleSuspense(paginatedQuery) + + return paginatedQuery } diff --git a/types/index.d.ts b/types/index.d.ts index 09a8d43992..d638c7eb63 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -11,7 +11,8 @@ import * as _ from 'ts-toolbelt' export function useQuery< TResult, TKey extends AnyQueryKey, - TVariables extends AnyVariables = [] + TVariables extends AnyVariables = [], + TError = Error >({ queryKey, variables, @@ -26,13 +27,14 @@ export function useQuery< | (() => TKey | false | null | undefined) variables?: TVariables queryFn: QueryFunctionWithVariables - config?: QueryOptions -}): QueryResult + config?: QueryOptions +}): QueryResult export function useQuery< TResult, TSingleKey extends string, - TVariables extends AnyVariables = [] + TVariables extends AnyVariables = [], + TError = Error >({ queryKey, variables, @@ -47,10 +49,10 @@ export function useQuery< | (() => TSingleKey | false | null | undefined) variables?: TVariables queryFn: QueryFunctionWithVariables - config?: QueryOptions -}): QueryResult + config?: QueryOptions +}): QueryResult -export function useQuery( +export function useQuery( queryKey: | TKey | false @@ -58,10 +60,10 @@ export function useQuery( | undefined | (() => TKey | false | null | undefined), queryFn: QueryFunction, - config?: QueryOptions -): QueryResult + config?: QueryOptions +): QueryResult -export function useQuery( +export function useQuery( queryKey: | TSingleKey | false @@ -69,13 +71,14 @@ export function useQuery( | undefined | (() => TSingleKey | false | null | undefined), queryFn: QueryFunction, - config?: QueryOptions -): QueryResult + config?: QueryOptions +): QueryResult export function useQuery< TResult, TKey extends AnyQueryKey, - TVariables extends AnyVariables + TVariables extends AnyVariables, + TError = Error >( queryKey: | TKey @@ -85,13 +88,14 @@ export function useQuery< | (() => TKey | false | null | undefined), variables: TVariables, queryFn: QueryFunctionWithVariables, - config?: QueryOptions -): QueryResult + config?: QueryOptions +): QueryResult export function useQuery< TResult, TKey extends string, - TVariables extends AnyVariables + TVariables extends AnyVariables, + TError = Error >( queryKey: | TKey @@ -101,14 +105,15 @@ export function useQuery< | (() => TKey | false | null | undefined), variables: TVariables, queryFn: QueryFunctionWithVariables, - config?: QueryOptions -): QueryResult + config?: QueryOptions +): QueryResult // usePaginatedQuery export function usePaginatedQuery< TResult, TKey extends AnyQueryKey, - TVariables extends AnyVariables = [] + TVariables extends AnyVariables = [], + TError = Error >({ queryKey, variables, @@ -123,13 +128,14 @@ export function usePaginatedQuery< | (() => TKey | false | null | undefined) variables?: TVariables queryFn: QueryFunctionWithVariables - config?: QueryOptions -}): PaginatedQueryResult + config?: QueryOptions +}): PaginatedQueryResult export function usePaginatedQuery< TResult, TSingleKey extends string, - TVariables extends AnyVariables = [] + TVariables extends AnyVariables = [], +TError = Error >({ queryKey, variables, @@ -144,10 +150,10 @@ export function usePaginatedQuery< | (() => TSingleKey | false | null | undefined) variables?: TVariables queryFn: QueryFunctionWithVariables - config?: QueryOptions -}): PaginatedQueryResult + config?: QueryOptions +}): PaginatedQueryResult -export function usePaginatedQuery( +export function usePaginatedQuery( queryKey: | TKey | false @@ -155,10 +161,10 @@ export function usePaginatedQuery( | undefined | (() => TKey | false | null | undefined), queryFn: QueryFunction, - config?: QueryOptions -): PaginatedQueryResult + config?: QueryOptions +): PaginatedQueryResult -export function usePaginatedQuery( +export function usePaginatedQuery( queryKey: | TKey | false @@ -166,13 +172,14 @@ export function usePaginatedQuery( | undefined | (() => TKey | false | null | undefined), queryFn: QueryFunction, - config?: QueryOptions -): PaginatedQueryResult + config?: QueryOptions +): PaginatedQueryResult export function usePaginatedQuery< TResult, TKey extends AnyQueryKey, - TVariables extends AnyVariables + TVariables extends AnyVariables, +TError = Error >( queryKey: | TKey @@ -182,13 +189,14 @@ export function usePaginatedQuery< | (() => TKey | false | null | undefined), variables: TVariables, queryFn: QueryFunctionWithVariables, - config?: QueryOptions -): PaginatedQueryResult + config?: QueryOptions +): PaginatedQueryResult export function usePaginatedQuery< TResult, TKey extends string, - TVariables extends AnyVariables + TVariables extends AnyVariables, + TError = Error >( queryKey: | TKey @@ -198,15 +206,16 @@ export function usePaginatedQuery< | (() => TKey | false | null | undefined), variables: TVariables, queryFn: QueryFunctionWithVariables, - config?: QueryOptions -): PaginatedQueryResult + config?: QueryOptions +): PaginatedQueryResult // useInfiniteQuery export function useInfiniteQuery< TResult, TKey extends AnyQueryKey, TMoreVariable, - TVariables extends AnyVariables = [] + TVariables extends AnyVariables = [], + TError = Error >({ queryKey, variables, @@ -226,14 +235,15 @@ export function useInfiniteQuery< TVariables, TMoreVariable > - config?: InfiniteQueryOptions -}): InfiniteQueryResult + config?: InfiniteQueryOptions +}): InfiniteQueryResult export function useInfiniteQuery< TResult, TSingleKey extends string, TMoreVariable, - TVariables extends AnyVariables = [] + TVariables extends AnyVariables = [], + TError = Error >({ queryKey, variables, @@ -253,13 +263,14 @@ export function useInfiniteQuery< TVariables, TMoreVariable > - config?: InfiniteQueryOptions -}): InfiniteQueryResult + config?: InfiniteQueryOptions +}): InfiniteQueryResult export function useInfiniteQuery< TResult, TKey extends AnyQueryKey, - TMoreVariable + TMoreVariable, + TError = Error >( queryKey: | TKey @@ -268,10 +279,10 @@ export function useInfiniteQuery< | undefined | (() => TKey | false | null | undefined), queryFn: InfiniteQueryFunction, - config?: InfiniteQueryOptions -): InfiniteQueryResult + config?: InfiniteQueryOptions +): InfiniteQueryResult -export function useInfiniteQuery( +export function useInfiniteQuery( queryKey: | TKey | false @@ -279,14 +290,15 @@ export function useInfiniteQuery( | undefined | (() => TKey | false | null | undefined), queryFn: InfiniteQueryFunction, - config?: InfiniteQueryOptions -): InfiniteQueryResult + config?: InfiniteQueryOptions +): InfiniteQueryResult export function useInfiniteQuery< TResult, TKey extends AnyQueryKey, TVariables extends AnyVariables, - TMoreVariable + TMoreVariable, + TError = Error >( queryKey: | TKey @@ -301,14 +313,15 @@ export function useInfiniteQuery< TVariables, TMoreVariable >, - config?: InfiniteQueryOptions -): InfiniteQueryResult + config?: InfiniteQueryOptions +): InfiniteQueryResult export function useInfiniteQuery< TResult, TKey extends string, TVariables extends AnyVariables, - TMoreVariable + TMoreVariable, + TError = Error >( queryKey: | TKey @@ -323,8 +336,8 @@ export function useInfiniteQuery< TVariables, TMoreVariable >, - config?: InfiniteQueryOptions -): InfiniteQueryResult + config?: InfiniteQueryOptions +): InfiniteQueryResult export type QueryKeyPart = | string @@ -369,7 +382,7 @@ export type InfiniteQueryFunctionWithVariables< | _.List.Concat ) => Promise -export interface BaseQueryOptions { +export interface BaseQueryOptions { /** * Set this to `true` to disable automatic refetching when the query mounts or changes query keys. * To refetch the query, use the `refetch` method returned from the `useQuery` instance. @@ -381,7 +394,7 @@ export interface BaseQueryOptions { * If set to an integer number, e.g. 3, failed queries will retry until the failed query count meets that number. * If set to a function `(failureCount, error) => boolean` failed queries will retry until the function returns false. */ - retry?: boolean | number | ((failureCount: number, error: unknown) => boolean) + retry?: boolean | number | ((failureCount: number, error: TError) => boolean) retryDelay?: (retryAttempt: number) => number staleTime?: number cacheTime?: number @@ -389,33 +402,33 @@ export interface BaseQueryOptions { refetchIntervalInBackground?: boolean refetchOnWindowFocus?: boolean refetchOnMount?: boolean - onError?: (err: unknown) => void + onError?: (err: TError) => void suspense?: boolean isDataEqual?: (oldData: unknown, newData: unknown) => boolean } -export interface QueryOptions extends BaseQueryOptions { +export interface QueryOptions extends BaseQueryOptions { onSuccess?: (data: TResult) => void - onSettled?: (data: TResult | undefined, error: unknown | null) => void + onSettled?: (data: TResult | undefined, error: TError | null) => void initialData?: TResult | (() => TResult | undefined) } -export interface PrefetchQueryOptions extends QueryOptions { +export interface PrefetchQueryOptions extends QueryOptions { force?: boolean throwOnError?: boolean } -export interface InfiniteQueryOptions - extends QueryOptions { +export interface InfiniteQueryOptions + extends QueryOptions { getFetchMore: ( lastPage: TResult, allPages: TResult[] ) => TMoreVariable | false } -export interface QueryResultBase { +export interface QueryResultBase { status: 'loading' | 'error' | 'success' - error: null | unknown + error: null | TError isFetching: boolean isStale: boolean failureCount: number @@ -428,16 +441,16 @@ export interface QueryResultBase { }) => Promise } -export interface QueryLoadingResult extends QueryResultBase { +export interface QueryLoadingResult extends QueryResultBase { status: 'loading' data: TResult | undefined // even when error, data can have stale data - error: unknown | null // it still can be error + error: TError | null // it still can be error } -export interface QueryErrorResult extends QueryResultBase { +export interface QueryErrorResult extends QueryResultBase { status: 'error' data: TResult | undefined // even when error, data can have stale data - error: unknown + error: TError } export interface QuerySuccessResult extends QueryResultBase { @@ -446,25 +459,25 @@ export interface QuerySuccessResult extends QueryResultBase { error: null } -export type QueryResult = - | QueryLoadingResult - | QueryErrorResult +export type QueryResult = + | QueryLoadingResult + | QueryErrorResult | QuerySuccessResult -export interface PaginatedQueryLoadingResult - extends QueryResultBase { +export interface PaginatedQueryLoadingResult + extends QueryResultBase { status: 'loading' resolvedData: undefined | TResult // even when error, data can have stale data latestData: undefined | TResult // even when error, data can have stale data - error: unknown | null // it still can be error + error: null | TError // it still can be error } -export interface PaginatedQueryErrorResult - extends QueryResultBase { +export interface PaginatedQueryErrorResult + extends QueryResultBase { status: 'error' resolvedData: undefined | TResult // even when error, data can have stale data latestData: undefined | TResult // even when error, data can have stale data - error: unknown + error: TError } export interface PaginatedQuerySuccessResult @@ -475,13 +488,13 @@ export interface PaginatedQuerySuccessResult error: null } -export type PaginatedQueryResult = - | PaginatedQueryLoadingResult - | PaginatedQueryErrorResult +export type PaginatedQueryResult = + | PaginatedQueryLoadingResult + | PaginatedQueryErrorResult | PaginatedQuerySuccessResult -export interface InfiniteQueryResult - extends QueryResultBase { +export interface InfiniteQueryResult + extends QueryResultBase { data: TResult[] isFetchingMore: boolean canFetchMore: boolean | undefined @@ -490,74 +503,74 @@ export interface InfiniteQueryResult ) => Promise | undefined } -export function useMutation( +export function useMutation( mutationFn: MutationFunction, - mutationOptions?: MutationOptions -): [MutateFunction, MutationResult] + mutationOptions?: MutationOptions +): [MutateFunction, MutationResult] export type MutationFunction = ( variables: TVariables ) => Promise -export interface MutateOptions { +export interface MutateOptions { onSuccess?: (data: TResult, variables: TVariables) => Promise | void onError?: ( - error: unknown, + error: TError, variables: TVariables, snapshotValue: unknown ) => Promise | void onSettled?: ( data: undefined | TResult, - error: unknown | null, + error: TError | null, variables: TVariables, snapshotValue?: unknown ) => Promise | void throwOnError?: boolean } -export interface MutationOptions - extends MutateOptions { +export interface MutationOptions + extends MutateOptions { onMutate?: (variables: TVariables) => Promise | unknown useErrorBoundary?: boolean } -export type MutateFunction = undefined extends TVariables +export type MutateFunction = undefined extends TVariables ? ( variables?: TVariables, - options?: MutateOptions + options?: MutateOptions ) => Promise : ( variables: TVariables, - options?: MutateOptions + options?: MutateOptions ) => Promise -export interface MutationResultBase { +export interface MutationResultBase { status: 'idle' | 'loading' | 'error' | 'success' data: undefined | TResult - error: null | unknown + error: undefined | null | TError promise: Promise reset: () => void } -export interface IdleMutationResult - extends MutationResultBase { +export interface IdleMutationResult + extends MutationResultBase { status: 'idle' data: undefined error: null } -export interface LoadingMutationResult - extends MutationResultBase { +export interface LoadingMutationResult + extends MutationResultBase { status: 'loading' data: undefined error: undefined } -export interface ErrorMutationResult - extends MutationResultBase { +export interface ErrorMutationResult + extends MutationResultBase { status: 'error' data: undefined - error: unknown + error: TError } export interface SuccessMutationResult @@ -567,15 +580,15 @@ export interface SuccessMutationResult error: undefined } -export type MutationResult = - | IdleMutationResult - | LoadingMutationResult - | ErrorMutationResult +export type MutationResult = + | IdleMutationResult + | LoadingMutationResult + | ErrorMutationResult | SuccessMutationResult export interface CachedQueryState { data?: T - error?: unknown | null + error?: Error | null failureCount: number isFetching: boolean canFetchMore?: boolean @@ -584,19 +597,20 @@ export interface CachedQueryState { updatedAt: number } -export interface CachedQuery { +export interface CachedQuery { queryKey: AnyQueryKey queryVariables: AnyVariables queryFn: (...args: any[]) => unknown - config: QueryOptions + config: QueryOptions state: CachedQueryState setData( dataOrUpdater: unknown | ((oldData: unknown | undefined) => unknown) ): void + clear(): void } export interface QueryCache { - prefetchQuery( + prefetchQuery( queryKey: | TKey | false @@ -604,10 +618,10 @@ export interface QueryCache { | undefined | (() => TKey | false | null | undefined), queryFn: QueryFunction, - config?: PrefetchQueryOptions + config?: PrefetchQueryOptions ): Promise - prefetchQuery( + prefetchQuery( queryKey: | TKey | false @@ -615,13 +629,14 @@ export interface QueryCache { | undefined | (() => TKey | false | null | undefined), queryFn: QueryFunction, - config?: PrefetchQueryOptions + config?: PrefetchQueryOptions ): Promise prefetchQuery< TResult, TKey extends AnyQueryKey, - TVariables extends AnyVariables + TVariables extends AnyVariables, + TError = Error >( queryKey: | TKey @@ -631,10 +646,10 @@ export interface QueryCache { | (() => TKey | false | null | undefined), variables: TVariables, queryFn: QueryFunctionWithVariables, - config?: PrefetchQueryOptions + config?: PrefetchQueryOptions ): Promise - prefetchQuery( + prefetchQuery( queryKey: | TKey | false @@ -643,13 +658,14 @@ export interface QueryCache { | (() => TKey | false | null | undefined), variables: TVariables, queryFn: QueryFunctionWithVariables, - config?: PrefetchQueryOptions + config?: PrefetchQueryOptions ): Promise prefetchQuery< TResult, TKey extends AnyQueryKey, - TVariables extends AnyVariables = [] + TVariables extends AnyVariables = [], + TError = Error >({ queryKey, variables, @@ -664,7 +680,7 @@ export interface QueryCache { | (() => TKey | false | null | undefined) variables?: TVariables queryFn: QueryFunctionWithVariables - config?: PrefetchQueryOptions + config?: PrefetchQueryOptions }): Promise getQueryData(key: AnyQueryKey | string): T | undefined @@ -701,7 +717,7 @@ export interface QueryCache { ): void isFetching: number subscribe(callback: (queryCache: QueryCache) => void): () => void - clear(): Array> + clear(): void } export const queryCache: QueryCache @@ -721,9 +737,9 @@ export const ReactQueryCacheProvider: React.ComponentType<{ }> /** - * A hook that returns the number of the quiries that your application is loading or fetching in the background + * A hook that returns the number of the queries that your application is loading or fetching in the background * (useful for app-wide loading indicators). - * @returns the number of the quiries that your application is currently loading or fetching in the background. + * @returns the number of the queries that your application is currently loading or fetching in the background. */ export function useIsFetching(): number @@ -731,7 +747,7 @@ export const ReactQueryConfigProvider: React.ComponentType<{ config?: ReactQueryProviderConfig }> -export interface ReactQueryProviderConfig extends BaseQueryOptions { +export interface ReactQueryProviderConfig extends BaseQueryOptions { /** Defaults to the value of `suspense` if not defined otherwise */ useErrorBoundary?: boolean throwOnError?: boolean @@ -747,10 +763,10 @@ export interface ReactQueryProviderConfig extends BaseQueryOptions { onMutate?: (variables: unknown) => Promise | unknown onSuccess?: (data: unknown, variables?: unknown) => void - onError?: (err: unknown, snapshotValue?: unknown) => void + onError?: (err: TError, snapshotValue?: unknown) => void onSettled?: ( data: unknown | undefined, - error: unknown | null, + error: TError | null, snapshotValue?: unknown ) => void isDataEqual?: (oldData: unknown, newData: unknown) => boolean diff --git a/types/test.ts b/types/test.ts index 5dd8549d7b..4ffd137404 100644 --- a/types/test.ts +++ b/types/test.ts @@ -12,7 +12,7 @@ function simpleQuery() { // Query - simple case const querySimple = useQuery('todos', () => Promise.resolve('test')) querySimple.data // $ExpectType string | undefined - querySimple.error // $ExpectType unknown + querySimple.error // $ExpectType Error | null querySimple.isFetching // $ExpectType boolean querySimple.refetch() // $ExpectType Promise querySimple.fetchMore // $ExpectError @@ -166,13 +166,13 @@ function paginatedQuery() { if (queryPaginated.status === 'loading') { queryPaginated.resolvedData // $ExpectType { data: number[]; next: boolean; } | undefined queryPaginated.latestData // $ExpectType { data: number[]; next: boolean; } | undefined - queryPaginated.error // $ExpectType unknown + queryPaginated.error // $ExpectType Error | null } if (queryPaginated.status === 'error') { queryPaginated.resolvedData // $ExpectType { data: number[]; next: boolean; } | undefined queryPaginated.latestData // $ExpectType { data: number[]; next: boolean; } | undefined - queryPaginated.error // $ExpectType unknown + queryPaginated.error // $ExpectType Error } if (queryPaginated.status === 'success') { @@ -221,7 +221,7 @@ function simpleInfiniteQuery(condition: boolean) { ) {}, onSettled( data, // $ExpectType number[][] | undefined - error // $ExpectType unknown + error // $ExpectType Error | null ) {}, initialData: () => condition @@ -331,7 +331,7 @@ function simpleMutation() { throwOnError: true, onSettled(result, error) { result // $ExpectType string[] | undefined - error // $ExpectType unknown + error // $ExpectType Error | null }, }) @@ -396,13 +396,13 @@ function dataDiscriminatedUnion() { // Discriminated union over status if (queryResult.status === 'loading') { queryResult.data // $ExpectType string[] | undefined - queryResult.error // $ExpectType unknown + queryResult.error // $ExpectType Error | null } if (queryResult.status === 'error') { // disabled queryResult.data // $ExpectType string[] | undefined - queryResult.error // $ExpectType unknown + queryResult.error // $ExpectType Error } if (queryResult.status === 'success') { @@ -437,7 +437,7 @@ function mutationStatusDiscriminatedUnion() { if (mutationState.status === 'error') { mutationState.data // $ExpectType undefined - mutationState.error // $ExpectType unknown + mutationState.error // $ExpectType Error } if (mutationState.status === 'success') {