Repository: tannerlinsley/react-query
Pull request #3518 (2022-04-16 13:52:53)

feat(query): functions for refetchOn options
refs #3110, #3517

vercel[bot]: **The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Updated |
| :--- | :----- | :------ | :------ |
| **react-query** | 🔄 Building ([Inspect](https://vercel.com/tanstack/react-query/EjV2Cbk9TmxeNMikp6atJnZGnUmT)) |  | Apr 16, 2022 at 1:52PM (UTC) |


codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/tannerlinsley/react-query/pr/3518/builds/240486) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit cafbee38e1f35a8dd6e60474b508c386bd1588ea:

|Sandbox| Source |
|--|--|
|[tannerlinsley/react-query: basic](https://codesandbox.io/s/tannerlinsley-react-query-basic-vziknx)| Configuration |
|[tannerlinsley/react-query: basic-typescript](https://codesandbox.io/s/tannerlinsley-react-query-basic-typescript-ni9ibq)| Configuration |

codecov[bot]: # [Codecov](https://codecov.io/gh/tannerlinsley/react-query/pull/3518?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) Report
> Merging [#3518](https://codecov.io/gh/tannerlinsley/react-query/pull/3518?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) (cafbee3) into [master](https://codecov.io/gh/tannerlinsley/react-query/commit/1ab13dc22efeb9270e38269efaa346e24e3fc0f6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) (1ab13dc) will **decrease** coverage by `0.01%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master    #3518      +/-   ##
==========================================
- Coverage   96.41%   96.39%   -0.02%     
==========================================
  Files          45       45              
  Lines        2286     2277       -9     
  Branches      642      637       -5     
==========================================
- Hits         2204     2195       -9     
  Misses         79       79              
  Partials        3        3              
```


| [Impacted Files](https://codecov.io/gh/tannerlinsley/react-query/pull/3518?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) | Coverage Δ | |
|---|---|---|
| [src/core/queryObserver.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3518/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvcXVlcnlPYnNlcnZlci50cw==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tannerlinsley/react-query/pull/3518?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tannerlinsley/react-query/pull/3518?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley). Last update [1ab13dc...cafbee3](https://codecov.io/gh/tannerlinsley/react-query/pull/3518?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley).


Patch:
diff --git a/docs/src/pages/guides/query-cancellation.md b/docs/src/pages/guides/query-cancellation.md
index ed57658b3..a067b63b7 100644
--- a/docs/src/pages/guides/query-cancellation.md
+++ b/docs/src/pages/guides/query-cancellation.md
@@ -129,8 +129,8 @@ You might want to cancel a query manually. For example, if the request takes a l
 ```js
 const [queryKey] = useState('todos')
 
-const query = useQuery(queryKey, await ({ signal }) => {
-  const resp = fetch('/todos', { signal })
+const query = useQuery(queryKey, async ({ signal }) => {
+  const resp = await fetch('/todos', { signal })
   return resp.json()
 })
 
diff --git a/docs/src/pages/reference/useQuery.md b/docs/src/pages/reference/useQuery.md
index 5731a20ab..e55ef3f4e 100644
--- a/docs/src/pages/reference/useQuery.md
+++ b/docs/src/pages/reference/useQuery.md
@@ -110,24 +110,27 @@ const result = useQuery({
 - `refetchIntervalInBackground: boolean`
   - Optional
   - If set to `true`, queries that are set to continuously refetch with a `refetchInterval` will continue to refetch while their tab/window is in the background
-- `refetchOnMount: boolean | "always"`
+- `refetchOnMount: boolean | "always" | ((query: Query) => boolean | "always")`
   - Optional
   - Defaults to `true`
   - If set to `true`, the query will refetch on mount if the data is stale.
   - If set to `false`, the query will not refetch on mount.
   - If set to `"always"`, the query will always refetch on mount.
-- `refetchOnWindowFocus: boolean | "always"`
+  - If set to a function, the function will be executed with the query to compute the value
+- `refetchOnWindowFocus: boolean | "always" | ((query: Query) => boolean | "always")`
   - Optional
   - Defaults to `true`
   - If set to `true`, the query will refetch on window focus if the data is stale.
   - If set to `false`, the query will not refetch on window focus.
   - If set to `"always"`, the query will always refetch on window focus.
-- `refetchOnReconnect: boolean | "always"`
+  - If set to a function, the function will be executed with the query to compute the value
+- `refetchOnReconnect: boolean | "always" | ((query: Query) => boolean | "always")`
   - Optional
   - Defaults to `true`
   - If set to `true`, the query will refetch on reconnect if the data is stale.
   - If set to `false`, the query will not refetch on reconnect.
   - If set to `"always"`, the query will always refetch on reconnect.
+  - If set to a function, the function will be executed with the query to compute the value
 - `notifyOnChangeProps: string[] | "tracked"`
   - Optional
   - If set, the component will only re-render if any of the listed properties change.
diff --git a/package.json b/package.json
index 560a5dcec..9215185f2 100644
--- a/package.json
+++ b/package.json
@@ -70,7 +70,7 @@
     "match-sorter": "^6.0.2"
   },
   "peerDependencies": {
-    "react": "^16.8.0 || ^17.0.0"
+    "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
   },
   "peerDependenciesMeta": {
     "react-dom": {
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index 77480e5c3..439752a9a 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -122,11 +122,19 @@ export class QueryObserver<
   }
 
   shouldFetchOnReconnect(): boolean {
-    return shouldFetchOnReconnect(this.currentQuery, this.options)
+    return shouldFetchOn(
+      this.currentQuery,
+      this.options,
+      this.options.refetchOnReconnect
+    )
   }
 
   shouldFetchOnWindowFocus(): boolean {
-    return shouldFetchOnWindowFocus(this.currentQuery, this.options)
+    return shouldFetchOn(
+      this.currentQuery,
+      this.options,
+      this.options.refetchOnWindowFocus
+    )
   }
 
   destroy(): void {
@@ -746,47 +754,30 @@ function shouldLoadOnMount(
   )
 }
 
-function shouldRefetchOnMount(
-  query: Query<any, any, any, any>,
-  options: QueryObserverOptions<any, any, any, any>
-): boolean {
-  return (
-    options.enabled !== false &&
-    query.state.dataUpdatedAt > 0 &&
-    (options.refetchOnMount === 'always' ||
-      (options.refetchOnMount !== false && isStale(query, options)))
-  )
-}
-
 function shouldFetchOnMount(
   query: Query<any, any, any, any>,
   options: QueryObserverOptions<any, any, any, any, any>
 ): boolean {
   return (
-    shouldLoadOnMount(query, options) || shouldRefetchOnMount(query, options)
+    shouldLoadOnMount(query, options) ||
+    (query.state.dataUpdatedAt > 0 &&
+      shouldFetchOn(query, options, options.refetchOnMount))
   )
 }
 
-function shouldFetchOnReconnect(
+function shouldFetchOn(
   query: Query<any, any, any, any>,
-  options: QueryObserverOptions<any, any, any, any, any>
-): boolean {
-  return (
-    options.enabled !== false &&
-    (options.refetchOnReconnect === 'always' ||
-      (options.refetchOnReconnect !== false && isStale(query, options)))
-  )
-}
+  options: QueryObserverOptions<any, any, any, any, any>,
+  field: typeof options['refetchOnMount'] &
+    typeof options['refetchOnWindowFocus'] &
+    typeof options['refetchOnReconnect']
+) {
+  if (options.enabled !== false) {
+    const value = typeof field === 'function' ? field(query) : field
 
-function shouldFetchOnWindowFocus(
-  query: Query<any, any, any, any>,
-  options: QueryObserverOptions<any, any, any, any, any>
-): boolean {
-  return (
-    options.enabled !== false &&
-    (options.refetchOnWindowFocus === 'always' ||
-      (options.refetchOnWindowFocus !== false && isStale(query, options)))
-  )
+    return value === 'always' || (value !== false && isStale(query, options))
+  }
+  return false
 }
 
 function shouldFetchOptionally(
diff --git a/src/core/types.ts b/src/core/types.ts
index 3ae8eba1e..25a2b9027 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -135,23 +135,41 @@ export interface QueryObserverOptions<
    * If set to `true`, the query will refetch on window focus if the data is stale.
    * If set to `false`, the query will not refetch on window focus.
    * If set to `'always'`, the query will always refetch on window focus.
+   * If set to a function, the function will be executed with the latest data and query to compute the value.
    * Defaults to `true`.
    */
-  refetchOnWindowFocus?: boolean | 'always'
+  refetchOnWindowFocus?:
+    | boolean
+    | 'always'
+    | ((
+        query: Query<TQueryFnData, TError, TQueryData, TQueryKey>
+      ) => boolean | 'always')
   /**
    * If set to `true`, the query will refetch on reconnect if the data is stale.
    * If set to `false`, the query will not refetch on reconnect.
    * If set to `'always'`, the query will always refetch on reconnect.
+   * If set to a function, the function will be executed with the latest data and query to compute the value.
    * Defaults to `true`.
    */
-  refetchOnReconnect?: boolean | 'always'
+  refetchOnReconnect?:
+    | boolean
+    | 'always'
+    | ((
+        query: Query<TQueryFnData, TError, TQueryData, TQueryKey>
+      ) => boolean | 'always')
   /**
    * If set to `true`, the query will refetch on mount if the data is stale.
    * If set to `false`, will disable additional instances of a query to trigger background refetches.
    * If set to `'always'`, the query will always refetch on mount.
+   * If set to a function, the function will be executed with the latest data and query to compute the value
    * Defaults to `true`.
    */
-  refetchOnMount?: boolean | 'always'
+  refetchOnMount?:
+    | boolean
+    | 'always'
+    | ((
+        query: Query<TQueryFnData, TError, TQueryData, TQueryKey>
+      ) => boolean | 'always')
   /**
    * If set to `false`, the query will not be retried on mount if it contains an error.
    * Defaults to `true`.
diff --git a/src/devtools/devtools.tsx b/src/devtools/devtools.tsx
index e740a5e57..9ae4ea53a 100644
--- a/src/devtools/devtools.tsx
+++ b/src/devtools/devtools.tsx
@@ -58,6 +58,10 @@ interface DevtoolsOptions {
    * Defaults to 'aside'.
    */
   containerElement?: string | any
+  /**
+   * nonce for style element for CSP
+   */
+  styleNonce?: string
 }
 
 interface DevtoolsPanelOptions {
@@ -73,6 +77,10 @@ interface DevtoolsPanelOptions {
    * A boolean variable indicating whether the panel is open or closed
    */
   isOpen?: boolean
+  /**
+   * nonce for style element for CSP
+   */
+  styleNonce?: string
   /**
    * A function that toggles the open and close state of the panel
    */
@@ -92,6 +100,7 @@ export function ReactQueryDevtools({
   toggleButtonProps = {},
   position = 'bottom-left',
   containerElement: Container = 'aside',
+  styleNonce,
 }: DevtoolsOptions): React.ReactElement | null {
   const rootRef = React.useRef<HTMLDivElement>(null)
   const panelRef = React.useRef<HTMLDivElement>(null)
@@ -229,6 +238,7 @@ export function ReactQueryDevtools({
       <ThemeProvider theme={theme}>
         <ReactQueryDevtoolsPanel
           ref={panelRef as any}
+          styleNonce={styleNonce}
           {...otherPanelProps}
           style={{
             position: 'fixed',
@@ -375,7 +385,13 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
   HTMLDivElement,
   DevtoolsPanelOptions
 >(function ReactQueryDevtoolsPanel(props, ref): React.ReactElement {
-  const { isOpen = true, setIsOpen, handleDragStart, ...panelProps } = props
+  const {
+    isOpen = true,
+    styleNonce,
+    setIsOpen,
+    handleDragStart,
+    ...panelProps
+  } = props
 
   const queryClient = useQueryClient()
   const queryCache = queryClient.getQueryCache()
@@ -467,6 +483,7 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
         {...panelProps}
       >
         <style
+          nonce={styleNonce}
           dangerouslySetInnerHTML={{
             __html: `
             .ReactQueryDevtoolsPanel * {
diff --git a/src/react/Hydrate.tsx b/src/react/Hydrate.tsx
index 522189740..5e5893e01 100644
--- a/src/react/Hydrate.tsx
+++ b/src/react/Hydrate.tsx
@@ -23,13 +23,10 @@ export function useHydrate(state: unknown, options?: HydrateOptions) {
 export interface HydrateProps {
   state?: unknown
   options?: HydrateOptions
+  children?: React.ReactNode
 }
 
-export const Hydrate: React.FC<HydrateProps> = ({
-  children,
-  options,
-  state,
-}) => {
+export const Hydrate = ({ children, options, state }: HydrateProps) => {
   useHydrate(state, options)
-  return children as React.ReactElement<any>
+  return children as React.ReactElement
 }
diff --git a/src/react/QueryClientProvider.tsx b/src/react/QueryClientProvider.tsx
index e0ea8baa5..d5e590f7d 100644
--- a/src/react/QueryClientProvider.tsx
+++ b/src/react/QueryClientProvider.tsx
@@ -44,13 +44,14 @@ export const useQueryClient = () => {
 export interface QueryClientProviderProps {
   client: QueryClient
   contextSharing?: boolean
+  children?: React.ReactNode
 }
 
-export const QueryClientProvider: React.FC<QueryClientProviderProps> = ({
+export const QueryClientProvider = ({
   client,
   contextSharing = false,
   children,
-}) => {
+}: QueryClientProviderProps): JSX.Element => {
   React.useEffect(() => {
     client.mount()
     return () => {
diff --git a/src/react/QueryErrorResetBoundary.tsx b/src/react/QueryErrorResetBoundary.tsx
index 284de240e..b380e275b 100644
--- a/src/react/QueryErrorResetBoundary.tsx
+++ b/src/react/QueryErrorResetBoundary.tsx
@@ -38,9 +38,9 @@ export interface QueryErrorResetBoundaryProps {
     | React.ReactNode
 }
 
-export const QueryErrorResetBoundary: React.FC<QueryErrorResetBoundaryProps> = ({
+export const QueryErrorResetBoundary = ({
   children,
-}) => {
+}: QueryErrorResetBoundaryProps) => {
   const value = React.useMemo(() => createValue(), [])
   return (
     <QueryErrorResetBoundaryContext.Provider value={value}>


Test patch:
diff --git a/src/devtools/tests/devtools.test.tsx b/src/devtools/tests/devtools.test.tsx
index b529052a0..4ee9820d6 100644
--- a/src/devtools/tests/devtools.test.tsx
+++ b/src/devtools/tests/devtools.test.tsx
@@ -391,4 +391,21 @@ describe('ReactQueryDevtools', () => {
     expect(queries[1]?.textContent).toEqual(query2Hash)
     expect(queries[2]?.textContent).toEqual(query3Hash)
   })
+
+  it('style should have a nonce', async () => {
+    const { queryClient } = createQueryClient()
+
+    function Page() {
+      return <div></div>
+    }
+
+    const { container } = renderWithClient(queryClient, <Page />, {
+      styleNonce: 'test-nonce',
+      initialIsOpen: false,
+    })
+    const styleTag = container.querySelector('style')
+    expect(styleTag).toHaveAttribute('nonce', 'test-nonce')
+
+    await screen.findByRole('button', { name: /react query devtools/i })
+  })
 })
diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx
index 3cc2facc9..e4d26912b 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -2401,6 +2401,35 @@ describe('useQuery', () => {
     expect(states[1]).toMatchObject({ data: 0, isFetching: false })
   })
 
+  it('should not refetch stale query on focus when `refetchOnWindowFocus` is set to a function that returns `false`', async () => {
+    const key = queryKey()
+    const states: UseQueryResult<number>[] = []
+    let count = 0
+
+    function Page() {
+      const state = useQuery(key, () => count++, {
+        staleTime: 0,
+        refetchOnWindowFocus: () => false,
+      })
+      states.push(state)
+      return null
+    }
+
+    renderWithClient(queryClient, <Page />)
+
+    await sleep(10)
+
+    act(() => {
+      window.dispatchEvent(new FocusEvent('focus'))
+    })
+
+    await sleep(10)
+
+    expect(states.length).toBe(2)
+    expect(states[0]).toMatchObject({ data: undefined, isFetching: true })
+    expect(states[1]).toMatchObject({ data: 0, isFetching: false })
+  })
+
   it('should not refetch fresh query on focus when `refetchOnWindowFocus` is set to `true`', async () => {
     const key = queryKey()
     const states: UseQueryResult<number>[] = []
@@ -2468,6 +2497,58 @@ describe('useQuery', () => {
     expect(states[3]).toMatchObject({ data: 1, isFetching: false })
   })
 
+  it('should calculate focus behaviour for `refetchOnWindowFocus` depending on function', async () => {
+    const key = queryKey()
+    const states: UseQueryResult<number>[] = []
+    let count = 0
+
+    function Page() {
+      const state = useQuery(
+        key,
+        async () => {
+          await sleep(1)
+          return count++
+        },
+        {
+          staleTime: 0,
+          retry: 0,
+          refetchOnWindowFocus: query => (query.state.data || 0) < 1,
+        }
+      )
+      states.push(state)
+      return null
+    }
+
+    renderWithClient(queryClient, <Page />)
+
+    await sleep(10)
+
+    expect(states.length).toBe(2)
+    expect(states[0]).toMatchObject({ data: undefined, isFetching: true })
+    expect(states[1]).toMatchObject({ data: 0, isFetching: false })
+
+    act(() => {
+      window.dispatchEvent(new FocusEvent('focus'))
+    })
+
+    await sleep(10)
+
+    // refetch should happen
+    expect(states.length).toBe(4)
+
+    expect(states[2]).toMatchObject({ data: 0, isFetching: true })
+    expect(states[3]).toMatchObject({ data: 1, isFetching: false })
+
+    act(() => {
+      window.dispatchEvent(new FocusEvent('focus'))
+    })
+
+    await sleep(10)
+
+    // no more refetch now
+    expect(states.length).toBe(4)
+  })
+
   it('should refetch fresh query when refetchOnMount is set to always', async () => {
     const key = queryKey()
     const states: UseQueryResult<string>[] = []
@@ -4677,4 +4758,38 @@ describe('useQuery', () => {
 
     consoleMock.mockRestore()
   })
+
+  it('it should have status=error on mount when a query has failed', async () => {
+    const consoleMock = mockConsoleError()
+    const key = queryKey()
+    const states: UseQueryResult<number>[] = []
+    const error = new Error('oops')
+
+    const queryFn = async () => {
+      throw error
+    }
+
+    function Page() {
+      const state = useQuery(key, queryFn, {
+        retry: false,
+        retryOnMount: false,
+      })
+
+      states.push(state)
+
+      return <></>
+    }
+
+    await queryClient.prefetchQuery(key, queryFn)
+    renderWithClient(queryClient, <Page />)
+
+    await waitFor(() => expect(states).toHaveLength(1))
+
+    expect(states[0]).toMatchObject({
+      status: 'error',
+      error,
+    })
+
+    consoleMock.mockRestore()
+  })
 })
diff --git a/src/react/tests/utils.tsx b/src/react/tests/utils.tsx
index 314c3183b..26bc71ac7 100644
--- a/src/react/tests/utils.tsx
+++ b/src/react/tests/utils.tsx
@@ -67,9 +67,12 @@ export const expectType = <T,>(_: T): void => undefined
 export const expectTypeNotAny = <T,>(_: 0 extends 1 & T ? never : T): void =>
   undefined
 
-export const Blink: React.FC<{ duration: number }> = ({
+export const Blink = ({
   duration,
   children,
+}: {
+  duration: number
+  children: React.ReactNode
 }) => {
   const [shouldShow, setShouldShow] = React.useState<boolean>(true)