Skip to content

feat: add QueryCache.fetchQuery method #1041

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 17, 2020
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
32 changes: 22 additions & 10 deletions docs/src/pages/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ const {
} = useQuery(queryKey, queryFn?, {
cacheTime,
enabled,
forceFetchOnMount,
initialData,
initialStale,
isDataEqual,
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -362,6 +357,7 @@ const queryCache = new QueryCache({

Its available properties and methods are:

- [`fetchQuery`](#querycachefetchquery)
- [`prefetchQuery`](#querycacheprefetchquery)
- [`getQueryData`](#querycachegetquerydata)
- [`setQueryData`](#querycachesetquerydata)
Expand All @@ -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<TResult>`

## `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
Expand All @@ -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,
})
Expand All @@ -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**
Expand All @@ -423,7 +435,7 @@ The options for `prefetchQuery` are exactly the same as those of [`useQuery`](#u

**Returns**

- `promise: Promise`
- `Promise<TResult | undefined>`
- 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`
Expand Down
6 changes: 2 additions & 4 deletions docs/src/pages/docs/guides/prefetching.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
```
Expand Down
12 changes: 6 additions & 6 deletions docs/src/pages/docs/guides/ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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**

Expand Down
25 changes: 14 additions & 11 deletions src/core/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export class Query<TResult, TError> {
cacheTime: number

private queryCache: QueryCache
private promise?: Promise<TResult | undefined>
private promise?: Promise<TResult>
private gcTimeout?: number
private cancelFetch?: (silent?: boolean) => void
private continueFetch?: () => void
Expand Down Expand Up @@ -160,12 +160,15 @@ export class Query<TResult, TError> {
}, this.cacheTime)
}

async cancel(silent?: boolean): Promise<void> {
cancel(silent?: boolean): Promise<undefined> {
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 {
Expand Down Expand Up @@ -316,17 +319,17 @@ export class Query<TResult, TError> {
}
}

async refetch(
refetch(
options?: RefetchOptions,
config?: ResolvedQueryConfig<TResult, TError>
): Promise<TResult | undefined> {
try {
return await this.fetch(undefined, config)
} catch (error) {
if (options?.throwOnError === true) {
throw error
}
let promise: Promise<TResult | undefined> = this.fetch(undefined, config)

if (!options?.throwOnError) {
promise = promise.catch(noop)
}

return promise
}

fetchMore(
Expand All @@ -348,7 +351,7 @@ export class Query<TResult, TError> {
async fetch(
options?: FetchOptions,
config?: ResolvedQueryConfig<TResult, TError>
): Promise<TResult | undefined> {
): Promise<TResult> {
if (this.promise) {
if (options?.fetchMore && this.state.data) {
// Silently cancel current fetch if the user wants to fetch more
Expand Down
121 changes: 88 additions & 33 deletions src/core/queryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
isOnline,
isPlainObject,
isServer,
noop,
} from './utils'
import { getResolvedQueryConfig } from './config'
import { Query } from './query'
Expand Down Expand Up @@ -55,6 +56,12 @@ type QueryPredicate = QueryKey | QueryPredicateFn | true

type QueryPredicateFn = (query: Query<unknown, unknown>) => boolean

export interface FetchQueryObjectConfig<TResult, TError> {
queryKey: QueryKey
queryFn?: QueryFunction<TResult>
config?: QueryConfig<TResult, TError>
}

export interface PrefetchQueryObjectConfig<TResult, TError> {
queryKey: QueryKey
queryFn?: QueryFunction<TResult>
Expand Down Expand Up @@ -302,87 +309,135 @@ export class QueryCache {
return query
}

// Parameter syntax
fetchQuery<TResult = unknown, TError = unknown>(
queryKey: QueryKey,
queryConfig?: QueryConfig<TResult, TError>
): Promise<TResult>

// Parameter syntax with query function
fetchQuery<TResult, TError, TArgs extends TypedQueryFunctionArgs>(
queryKey: QueryKey,
queryFn: TypedQueryFunction<TResult, TArgs>,
queryConfig?: QueryConfig<TResult, TError>
): Promise<TResult>

fetchQuery<TResult = unknown, TError = unknown>(
queryKey: QueryKey,
queryFn: QueryFunction<TResult>,
queryConfig?: QueryConfig<TResult, TError>
): Promise<TResult>

// Object syntax
fetchQuery<TResult = unknown, TError = unknown>(
config: FetchQueryObjectConfig<TResult, TError>
): Promise<TResult>

// Implementation
fetchQuery<TResult, TError>(
arg1: any,
arg2?: any,
arg3?: any
): Promise<TResult> {
const [queryKey, config] = getQueryArgs<TResult, TError>(arg1, arg2, arg3)

const resolvedConfig = this.getResolvedQueryConfig(queryKey, {
// https://github.com/tannerlinsley/react-query/issues/652
retry: false,
...config,
})

let query = this.getQueryByHash<TResult, TError>(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<TResult = unknown, TError = unknown>(
prefetchQuery<TResult = unknown, TError = unknown>(
queryKey: QueryKey,
options?: PrefetchQueryOptions
): Promise<TResult | undefined>

// Parameter syntax with query function and optional prefetch options
async prefetchQuery<TResult, TError, TArgs extends TypedQueryFunctionArgs>(
prefetchQuery<TResult, TError, TArgs extends TypedQueryFunctionArgs>(
queryKey: QueryKey,
queryFn: TypedQueryFunction<TResult, TArgs>,
options?: PrefetchQueryOptions
): Promise<TResult | undefined>

async prefetchQuery<TResult = unknown, TError = unknown>(
prefetchQuery<TResult = unknown, TError = unknown>(
queryKey: QueryKey,
queryFn: QueryFunction<TResult>,
options?: PrefetchQueryOptions
): Promise<TResult | undefined>

// Parameter syntax with query function, config and optional prefetch options
async prefetchQuery<TResult, TError, TArgs extends TypedQueryFunctionArgs>(
prefetchQuery<TResult, TError, TArgs extends TypedQueryFunctionArgs>(
queryKey: QueryKey,
queryFn: TypedQueryFunction<TResult, TArgs>,
queryConfig: QueryConfig<TResult, TError>,
options?: PrefetchQueryOptions
): Promise<TResult | undefined>

async prefetchQuery<TResult = unknown, TError = unknown>(
prefetchQuery<TResult = unknown, TError = unknown>(
queryKey: QueryKey,
queryFn: QueryFunction<TResult>,
queryConfig: QueryConfig<TResult, TError>,
options?: PrefetchQueryOptions
): Promise<TResult | undefined>

// Object syntax
async prefetchQuery<TResult = unknown, TError = unknown>(
prefetchQuery<TResult = unknown, TError = unknown>(
config: PrefetchQueryObjectConfig<TResult, TError>
): Promise<TResult | undefined>

// Implementation
async prefetchQuery<TResult, TError>(
...args: any[]
prefetchQuery<TResult, TError>(
arg1: any,
arg2?: any,
arg3?: any,
arg4?: any
): Promise<TResult | undefined> {
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<TResult, TError>(resolvedConfig.queryHash)
let promise: Promise<TResult | undefined> = 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<TResult, TError = unknown>(
Expand Down
Loading