diff --git a/docs/src/pages/docs/api.md b/docs/src/pages/docs/api.md index 39237b634a..9d02cf1972 100644 --- a/docs/src/pages/docs/api.md +++ b/docs/src/pages/docs/api.md @@ -24,7 +24,6 @@ const { } = useQuery(queryKey, queryFn?, { cacheTime, enabled, - forceFetchOnMount, initialData, initialStale, isDataEqual, @@ -142,10 +141,6 @@ const queryInfo = useQuery({ - Optional - Defaults to `false` - If set, any previous `data` will be kept when fetching new data because the query key changed. -- `forceFetchOnMount: Boolean` - - Optional - - Defaults to `false` - - Set this to `true` to always fetch when the component mounts (regardless of staleness). - `queryFnParamsFilter: Function(args) => filteredArgs` - Optional - This function will filter the params that get passed to `queryFn`. @@ -362,6 +357,7 @@ const queryCache = new QueryCache({ Its available properties and methods are: +- [`fetchQuery`](#querycachefetchquery) - [`prefetchQuery`](#querycacheprefetchquery) - [`getQueryData`](#querycachegetquerydata) - [`setQueryData`](#querycachesetquerydata) @@ -380,9 +376,25 @@ Its available properties and methods are: - Optional - Define defaults for all queries and mutations using this query cache. +## `queryCache.fetchQuery` + +`fetchQuery` is an asynchronous method that can be used to fetch and cache a query. It will either resolve with the data or throw with the error. Specify a `staleTime` to only trigger a fetch when the data is stale. Use the `prefetchQuery` method if you just want to fetch a query without needing the result. + +```js +try { + const data = await queryCache.fetchQuery(queryKey, queryFn) +} catch (error) { + console.log(error) +} +``` + +**Returns** + +- `Promise` + ## `queryCache.prefetchQuery` -`prefetchQuery` is an asynchronous function that can be used to fetch and cache a query response before it is needed or rendered with `useQuery` and friends. +`prefetchQuery` is an asynchronous method that can be used to fetch and cache a query response before it is needed or rendered with `useQuery` and friends. - If either: - The query does not exist or @@ -394,13 +406,13 @@ Its available properties and methods are: > The difference between using `prefetchQuery` and `setQueryData` is that `prefetchQuery` is async and will ensure that duplicate requests for this query are not created with `useQuery` instances for the same query are rendered while the data is fetching. ```js -const data = await queryCache.prefetchQuery(queryKey, queryFn) +await queryCache.prefetchQuery(queryKey, queryFn) ``` To pass options like `force` or `throwOnError`, use the fourth options object: ```js -const data = await queryCache.prefetchQuery(queryKey, queryFn, config, { +await queryCache.prefetchQuery(queryKey, queryFn, config, { force: true, throwOnError: true, }) @@ -409,7 +421,7 @@ const data = await queryCache.prefetchQuery(queryKey, queryFn, config, { You can even use it with a default queryFn in your config! ```js -const data = await queryCache.prefetchQuery(queryKey) +await queryCache.prefetchQuery(queryKey) ``` **Options** @@ -423,7 +435,7 @@ The options for `prefetchQuery` are exactly the same as those of [`useQuery`](#u **Returns** -- `promise: Promise` +- `Promise` - A promise is returned that will either immediately resolve with the query's cached response data, or resolve to the data returned by the fetch function. It **will not** throw an error if the fetch fails. This can be configured by setting the `throwOnError` option to `true`. ## `queryCache.getQueryData` diff --git a/docs/src/pages/docs/guides/prefetching.md b/docs/src/pages/docs/guides/prefetching.md index 2a868f3415..2e6498b307 100644 --- a/docs/src/pages/docs/guides/prefetching.md +++ b/docs/src/pages/docs/guides/prefetching.md @@ -3,13 +3,11 @@ id: prefetching title: 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, you can 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 `fetchQuery` or `prefetchQuery` methods to prefetch the results of a query to be placed into the cache: ```js const prefetchTodos = async () => { - const queryData = await queryCache.prefetchQuery('todos', () => - fetch('/todos') - ) + await queryCache.prefetchQuery('todos', () => fetch('/todos')) // The results of this query will be cached like a normal query } ``` diff --git a/docs/src/pages/docs/guides/ssr.md b/docs/src/pages/docs/guides/ssr.md index 797bebf8a7..d398c51695 100644 --- a/docs/src/pages/docs/guides/ssr.md +++ b/docs/src/pages/docs/guides/ssr.md @@ -118,12 +118,12 @@ Since there are many different possible setups for SSR, it's hard to give a deta > Note: The global `queryCache` you can import directly from 'react-query' does not cache queries on the server to avoid leaking sensitive information between requests. - Prefetch data - - Create a `prefetchQueryCache` specifically for prefetching by calling `const prefetchQueryCache = new QueryCache()` - - Call `prefetchQueryCache.prefetchQuery(...)` to prefetch queries - - Dehydrate by using `const dehydratedState = dehydrate(prefetchQueryCache)` + - Create a `prefetchCache` specifically for prefetching by calling `const prefetchCache = new QueryCache()` + - Call `prefetchCache.prefetchQuery(...)` to prefetch queries + - Dehydrate by using `const dehydratedState = dehydrate(prefetchCache)` - Render - - Create a new render query cache and hydrate the state. Use this query cache to render your app. - - **Do not** use the `prefetchQueryCache` to render your app, the server and client both needs to render from the dehydrated data to avoid React hydration mismatches. This is because queries with errors are excluded from dehydration by default. + - Create a new query cache for rendering and hydrate the state. Use this query cache to render your app. + - **Do not** use the `prefetchCache` to render your app, the server and client both needs to render from the dehydrated data to avoid React hydration mismatches. This is because queries with errors are excluded from dehydration by default. - Serialize and embed `dehydratedState` in the markup - Security note: Serializing data with `JSON.stringify` can put you at risk for XSS-vulnerabilities, [this blog post explains why and how to solve it](https://medium.com/node-security/the-most-common-xss-vulnerability-in-react-js-applications-2bdffbcc1fa0) @@ -180,7 +180,7 @@ ReactDOM.hydrate( Any query with an error is automatically excluded from dehydration. This means that the default behaviour is to pretend these queries were never loaded on the server, usually showing a loading state instead, and retrying the queries on the client. This happens regardless of error. -Sometimes this behavior is not desirable, maybe you want to render an error page with a correct status code instead on certain errors or queries. In those cases, pass `throwOnError: true` to the specific `prefetchQuery` to be able to catch and handle those errors manually. +Sometimes this behavior is not desirable, maybe you want to render an error page with a correct status code instead on certain errors or queries. In those cases, use `fetchQuery` and catch any errors to handle those manually. **Staleness is measured from when the query was fetched on the server** diff --git a/src/core/query.ts b/src/core/query.ts index b61bae1785..0e5173e565 100644 --- a/src/core/query.ts +++ b/src/core/query.ts @@ -110,7 +110,7 @@ export class Query { cacheTime: number private queryCache: QueryCache - private promise?: Promise + private promise?: Promise private gcTimeout?: number private cancelFetch?: (silent?: boolean) => void private continueFetch?: () => void @@ -160,12 +160,15 @@ export class Query { }, this.cacheTime) } - async cancel(silent?: boolean): Promise { + cancel(silent?: boolean): Promise { const promise = this.promise + if (promise && this.cancelFetch) { this.cancelFetch(silent) - await promise.catch(noop) + return promise.then(noop).catch(noop) } + + return Promise.resolve(undefined) } private continue(): void { @@ -316,17 +319,17 @@ export class Query { } } - async refetch( + refetch( options?: RefetchOptions, config?: ResolvedQueryConfig ): Promise { - try { - return await this.fetch(undefined, config) - } catch (error) { - if (options?.throwOnError === true) { - throw error - } + let promise: Promise = this.fetch(undefined, config) + + if (!options?.throwOnError) { + promise = promise.catch(noop) } + + return promise } fetchMore( @@ -348,7 +351,7 @@ export class Query { async fetch( options?: FetchOptions, config?: ResolvedQueryConfig - ): Promise { + ): Promise { if (this.promise) { if (options?.fetchMore && this.state.data) { // Silently cancel current fetch if the user wants to fetch more diff --git a/src/core/queryCache.ts b/src/core/queryCache.ts index cf98dc415d..3427001715 100644 --- a/src/core/queryCache.ts +++ b/src/core/queryCache.ts @@ -6,6 +6,7 @@ import { isOnline, isPlainObject, isServer, + noop, } from './utils' import { getResolvedQueryConfig } from './config' import { Query } from './query' @@ -55,6 +56,12 @@ type QueryPredicate = QueryKey | QueryPredicateFn | true type QueryPredicateFn = (query: Query) => boolean +export interface FetchQueryObjectConfig { + queryKey: QueryKey + queryFn?: QueryFunction + config?: QueryConfig +} + export interface PrefetchQueryObjectConfig { queryKey: QueryKey queryFn?: QueryFunction @@ -302,34 +309,88 @@ export class QueryCache { return query } + // Parameter syntax + fetchQuery( + queryKey: QueryKey, + queryConfig?: QueryConfig + ): Promise + + // Parameter syntax with query function + fetchQuery( + queryKey: QueryKey, + queryFn: TypedQueryFunction, + queryConfig?: QueryConfig + ): Promise + + fetchQuery( + queryKey: QueryKey, + queryFn: QueryFunction, + queryConfig?: QueryConfig + ): Promise + + // Object syntax + fetchQuery( + config: FetchQueryObjectConfig + ): Promise + + // Implementation + fetchQuery( + arg1: any, + arg2?: any, + arg3?: any + ): Promise { + const [queryKey, config] = getQueryArgs(arg1, arg2, arg3) + + const resolvedConfig = this.getResolvedQueryConfig(queryKey, { + // https://github.com/tannerlinsley/react-query/issues/652 + retry: false, + ...config, + }) + + let query = this.getQueryByHash(resolvedConfig.queryHash) + + if (!query) { + query = this.createQuery(resolvedConfig) + } + + if (!query.isStaleByTime(config.staleTime)) { + return Promise.resolve(query.state.data as TResult) + } + + return query.fetch(undefined, resolvedConfig) + } + + /** + * @deprecated + */ // Parameter syntax with optional prefetch options - async prefetchQuery( + prefetchQuery( queryKey: QueryKey, options?: PrefetchQueryOptions ): Promise // Parameter syntax with query function and optional prefetch options - async prefetchQuery( + prefetchQuery( queryKey: QueryKey, queryFn: TypedQueryFunction, options?: PrefetchQueryOptions ): Promise - async prefetchQuery( + prefetchQuery( queryKey: QueryKey, queryFn: QueryFunction, options?: PrefetchQueryOptions ): Promise // Parameter syntax with query function, config and optional prefetch options - async prefetchQuery( + prefetchQuery( queryKey: QueryKey, queryFn: TypedQueryFunction, queryConfig: QueryConfig, options?: PrefetchQueryOptions ): Promise - async prefetchQuery( + prefetchQuery( queryKey: QueryKey, queryFn: QueryFunction, queryConfig: QueryConfig, @@ -337,52 +398,46 @@ export class QueryCache { ): Promise // Object syntax - async prefetchQuery( + prefetchQuery( config: PrefetchQueryObjectConfig ): Promise // Implementation - async prefetchQuery( - ...args: any[] + prefetchQuery( + arg1: any, + arg2?: any, + arg3?: any, + arg4?: any ): Promise { if ( - isPlainObject(args[1]) && - (args[1].hasOwnProperty('throwOnError') || - args[1].hasOwnProperty('force')) + isPlainObject(arg2) && + (arg2.hasOwnProperty('throwOnError') || arg2.hasOwnProperty('force')) ) { - args[3] = args[1] - args[1] = undefined - args[2] = undefined + arg4 = arg2 + arg2 = undefined + arg3 = undefined } const [queryKey, config, options] = getQueryArgs< TResult, TError, PrefetchQueryOptions | undefined - >(args) + >(arg1, arg2, arg3, arg4) - const resolvedConfig = this.getResolvedQueryConfig(queryKey, { - // https://github.com/tannerlinsley/react-query/issues/652 - retry: false, - ...config, - }) + if (options?.force) { + config.staleTime = 0 + } - let query = this.getQueryByHash(resolvedConfig.queryHash) + let promise: Promise = this.fetchQuery( + queryKey, + config + ) - if (!query) { - query = this.createQuery(resolvedConfig) + if (!options?.throwOnError) { + promise = promise.catch(noop) } - try { - if (options?.force || query.isStaleByTime(config.staleTime)) { - await query.fetch(undefined, resolvedConfig) - } - return query.state.data - } catch (error) { - if (options?.throwOnError) { - throw error - } - } + return promise } setQueryData( diff --git a/src/core/tests/queryCache.test.tsx b/src/core/tests/queryCache.test.tsx index 3b83c51ee8..4fec201012 100644 --- a/src/core/tests/queryCache.test.tsx +++ b/src/core/tests/queryCache.test.tsx @@ -32,73 +32,61 @@ describe('queryCache', () => { }) // https://github.com/tannerlinsley/react-query/issues/652 - test('prefetchQuery should not retry by default', async () => { + test('fetchQuery should not retry by default', async () => { const consoleMock = mockConsoleError() const key = queryKey() await expect( - defaultQueryCache.prefetchQuery( - key, - async () => { - throw new Error('error') - }, - {}, - { throwOnError: true } - ) + defaultQueryCache.fetchQuery(key, async () => { + throw new Error('error') + }) ).rejects.toEqual(new Error('error')) consoleMock.mockRestore() }) - test('prefetchQuery returns the cached data on cache hits', async () => { + test('fetchQuery returns the cached data on cache hits', async () => { const key = queryKey() const fetchFn = () => Promise.resolve('data') - const first = await defaultQueryCache.prefetchQuery(key, fetchFn) - const second = await defaultQueryCache.prefetchQuery(key, fetchFn) + const first = await defaultQueryCache.fetchQuery(key, fetchFn) + const second = await defaultQueryCache.fetchQuery(key, fetchFn) expect(second).toBe(first) }) - test('prefetchQuery should not force fetch', async () => { + test('fetchQuery should not force fetch', async () => { const key = queryKey() defaultQueryCache.setQueryData(key, 'og', { staleTime: 100 }) const fetchFn = () => Promise.resolve('new') - const first = await defaultQueryCache.prefetchQuery( - key, - fetchFn, - { - initialData: 'initial', - staleTime: 100, - }, - { - throwOnError: true, - } - ) + const first = await defaultQueryCache.fetchQuery(key, fetchFn, { + initialData: 'initial', + staleTime: 100, + }) expect(first).toBe('og') }) - test('prefetchQuery should only fetch if the data is older then the given stale time', async () => { + test('fetchQuery should only fetch if the data is older then the given stale time', async () => { const key = queryKey() let count = 0 const fetchFn = () => ++count defaultQueryCache.setQueryData(key, count) - const first = await defaultQueryCache.prefetchQuery(key, fetchFn, { + const first = await defaultQueryCache.fetchQuery(key, fetchFn, { staleTime: 100, }) await sleep(11) - const second = await defaultQueryCache.prefetchQuery(key, fetchFn, { + const second = await defaultQueryCache.fetchQuery(key, fetchFn, { staleTime: 10, }) - const third = await defaultQueryCache.prefetchQuery(key, fetchFn, { + const third = await defaultQueryCache.fetchQuery(key, fetchFn, { staleTime: 10, }) await sleep(11) - const fourth = await defaultQueryCache.prefetchQuery(key, fetchFn, { + const fourth = await defaultQueryCache.fetchQuery(key, fetchFn, { staleTime: 10, }) expect(first).toBe(0) @@ -419,7 +407,7 @@ describe('queryCache', () => { let count = 0 let result - const promise = defaultQueryCache.prefetchQuery( + const promise = defaultQueryCache.fetchQuery( key, async () => { count++ @@ -467,7 +455,7 @@ describe('queryCache', () => { let count = 0 let result - const promise = defaultQueryCache.prefetchQuery( + const promise = defaultQueryCache.fetchQuery( key, async () => { count++ @@ -518,7 +506,7 @@ describe('queryCache', () => { let count = 0 let result - const promise = defaultQueryCache.prefetchQuery( + const promise = defaultQueryCache.fetchQuery( key, async () => { count++ @@ -527,9 +515,6 @@ describe('queryCache', () => { { retry: 3, retryDelay: 1, - }, - { - throwOnError: true, } ) @@ -626,17 +611,10 @@ describe('queryCache', () => { let error - const promise = defaultQueryCache.prefetchQuery( - key, - queryFn, - { - retry: 3, - retryDelay: 10, - }, - { - throwOnError: true, - } - ) + const promise = defaultQueryCache.fetchQuery(key, queryFn, { + retry: 3, + retryDelay: 10, + }) promise.catch(e => { error = e diff --git a/src/core/utils.ts b/src/core/utils.ts index ddbce0d6cc..dcaedf3a82 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -113,27 +113,30 @@ export function isOnline(): boolean { } export function getQueryArgs( - args: any[] + arg1: any, + arg2?: any, + arg3?: any, + arg4?: any ): [QueryKey, QueryConfig, TOptions] { let queryKey: QueryKey let queryFn: QueryFunction | undefined let config: QueryConfig | undefined let options: TOptions - if (isPlainObject(args[0])) { - queryKey = args[0].queryKey - queryFn = args[0].queryFn - config = args[0].config - options = args[1] - } else if (isPlainObject(args[1])) { - queryKey = args[0] - config = args[1] - options = args[2] + if (isPlainObject(arg1)) { + queryKey = arg1.queryKey + queryFn = arg1.queryFn + config = arg1.config + options = arg2 + } else if (isPlainObject(arg2)) { + queryKey = arg1 + config = arg2 + options = arg3 } else { - queryKey = args[0] - queryFn = args[1] - config = args[2] - options = args[3] + queryKey = arg1 + queryFn = arg2 + config = arg3 + options = arg4 } config = config || {} diff --git a/src/hydration/tests/hydration.test.tsx b/src/hydration/tests/hydration.test.tsx index 3dd89bc911..97489c6889 100644 --- a/src/hydration/tests/hydration.test.tsx +++ b/src/hydration/tests/hydration.test.tsx @@ -51,7 +51,9 @@ describe('dehydration and rehydration', () => { await hydrationQueryCache.prefetchQuery( 'boolean', fetchDataAfterHydration, - { staleTime: 1000 } + { + staleTime: 1000, + } ) await hydrationQueryCache.prefetchQuery('null', fetchDataAfterHydration, { staleTime: 1000, diff --git a/src/react/tests/ssr.test.tsx b/src/react/tests/ssr.test.tsx index d4dd042922..fbd98261be 100644 --- a/src/react/tests/ssr.test.tsx +++ b/src/react/tests/ssr.test.tsx @@ -26,7 +26,7 @@ describe('Server Side Rendering', () => { const key = queryKey() const fetchFn = () => Promise.resolve('data') - const data = await queryCache.prefetchQuery(key, fetchFn) + const data = await queryCache.fetchQuery(key, fetchFn) expect(data).toBe('data') expect(queryCache.getQuery(key)).toBeFalsy() @@ -41,7 +41,7 @@ describe('Server Side Rendering', () => { const cache = new QueryCache() const fetchFn = () => Promise.resolve('data') - const data = await cache.prefetchQuery(key, fetchFn) + const data = await cache.fetchQuery(key, fetchFn) expect(data).toBe('data') expect(cache.getQuery(key)).toBeTruthy() @@ -109,7 +109,7 @@ describe('Server Side Rendering', () => { const cache = new QueryCache({ frozen: true }) const fetchFn = () => Promise.resolve('data') - const data = await cache.prefetchQuery(key, fetchFn) + const data = await cache.fetchQuery(key, fetchFn) expect(data).toBe('data') expect(cache.getQuery(key)).toBeFalsy() @@ -150,7 +150,7 @@ describe('Server Side Rendering', () => { const cache = new QueryCache({ frozen: false }) const fetchFn = () => Promise.resolve('data') - const data = await cache.prefetchQuery(key, fetchFn) + const data = await cache.fetchQuery(key, fetchFn) expect(data).toBe('data') expect(cache.getQuery(key)?.state.data).toBe('data') diff --git a/src/react/useInfiniteQuery.ts b/src/react/useInfiniteQuery.ts index 799e2236bd..cc79eba5e4 100644 --- a/src/react/useInfiniteQuery.ts +++ b/src/react/useInfiniteQuery.ts @@ -49,8 +49,10 @@ export function useInfiniteQuery( // Implementation export function useInfiniteQuery( - ...args: any[] + arg1: any, + arg2?: any, + arg3?: any ): InfiniteQueryResult { - const [queryKey, config] = getQueryArgs(args) + const [queryKey, config] = getQueryArgs(arg1, arg2, arg3) return useBaseQuery(queryKey, { ...config, infinite: true }) } diff --git a/src/react/usePaginatedQuery.ts b/src/react/usePaginatedQuery.ts index 0ee71c8f9d..9418f84ace 100644 --- a/src/react/usePaginatedQuery.ts +++ b/src/react/usePaginatedQuery.ts @@ -54,9 +54,11 @@ export function usePaginatedQuery( // Implementation export function usePaginatedQuery( - ...args: any[] + arg1: any, + arg2?: any, + arg3?: any ): PaginatedQueryResult { - const [queryKey, config] = getQueryArgs(args) + const [queryKey, config] = getQueryArgs(arg1, arg2, arg3) const result = useBaseQuery(queryKey, { keepPreviousData: true, ...config, diff --git a/src/react/useQuery.ts b/src/react/useQuery.ts index 8e0fbee44b..d3f36bf4bb 100644 --- a/src/react/useQuery.ts +++ b/src/react/useQuery.ts @@ -45,8 +45,10 @@ export function useQuery( // Implementation export function useQuery( - ...args: any[] + arg1: any, + arg2?: any, + arg3?: any ): QueryResult { - const [queryKey, config] = getQueryArgs(args) + const [queryKey, config] = getQueryArgs(arg1, arg2, arg3) return useBaseQuery(queryKey, config) }