Repository: tannerlinsley/react-query
Pull request #2213 (2021-04-29 22:53:20)

refactor(devtools): move default theme to theme file, fix types
Move the default theme to the proper file.
Fix types for `theme.tsx`
No functional changes.

vercel[bot]: This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).  
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/tannerlinsley/react-query/9ftw2wfZsynUeZSPcWXj9HfCcejR](https://vercel.com/tannerlinsley/react-query/9ftw2wfZsynUeZSPcWXj9HfCcejR)  
✅ Preview: *In Progress*


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/2213/builds/125454) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 33ed932a6230a368bd27db4ff6ec395399ebe2b4:

|Sandbox| Source |
|--|--|
|[tannerlinsley/react-query: basic](https://codesandbox.io/s/tannerlinsleyreact-query-basic-y5jg9)| Configuration |
|[tannerlinsley/react-query: basic-typescript](https://codesandbox.io/s/tannerlinsleyreact-query-basic-typescript-5uyms)| Configuration |

TkDodo: have a look at the auto-deployed codesandboxes please - I can't see the devtools anymore :/

https://codesandbox.io/s/tannerlinsleyreact-query-basic-y5jg9
DamianOsipiuk: That is very weird, as it worked fine when I was checking locally.
I will double-check locally, and if I would not be able to find the issue, then maybe deployment to codesandbox somehow got broken.
DamianOsipiuk: @TkDodo Should be fixed now

Patch:
diff --git a/docs/src/pages/reference/QueryClient.md b/docs/src/pages/reference/QueryClient.md
index e7d885a47..559bc641f 100644
--- a/docs/src/pages/reference/QueryClient.md
+++ b/docs/src/pages/reference/QueryClient.md
@@ -29,6 +29,7 @@ Its available methods are:
 - [`prefetchInfiniteQuery`](#queryclientprefetchinfinitequery)
 - [`getQueryData`](#queryclientgetquerydata)
 - [`setQueryData`](#queryclientsetquerydata)
+- [`setQueriesData`](#queryclientsetqueriesdata)
 - [`getQueryState`](#queryclientgetquerystate)
 - [`invalidateQueries`](#queryclientinvalidatequeries)
 - [`refetchQueries`](#queryclientrefetchqueries)
@@ -187,7 +188,7 @@ queryClient.setQueryData(queryKey, updater)
 **Options**
 
 - `queryKey: QueryKey`: [Query Keys](../guides/query-keys)
-- `updater: unknown | (oldData: TData | undefined) => TData`
+- `updater: TData | (oldData: TData | undefined) => TData`
   - If non-function is passed, the data will be updated to this value
   - If a function is passed, it will receive the old data value and be expected to return a new one.
 
@@ -219,6 +220,22 @@ console.log(state.dataUpdatedAt)
 - `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
 - `filters?: QueryFilters`: [Query Filters](../guides/filters#query-filters)
 
+## `queryClient.setQueriesData`
+
+`setQueriesData` is a synchronous function that can be used to immediately update cached data of multiple queries. Only queries that match the passed queryKey or queryFilter will be updated - no new cache entries will be created. Under the hood, [`setQueryData`](#queryclientsetquerydata) is called for each query.
+
+```js
+queryClient.setQueriesData(queryKey | filters, updater)
+```
+
+**Options**
+
+- `queryKey: QueryKey`: [Query Keys](../guides/query-keys) | `filters: QueryFilters`: [Query Filters](../guides/filters#query-filters)
+  - if a queryKey is passed as first argument, queryKeys fuzzily matching this param will be updated
+  - if a filter is passed, queryKeys matching the filter will be updated
+- `updater: TData | (oldData: TData | undefined) => TData`
+  - the [setQueryData](#queryclientsetquerydata) updater function or new data, will be called for each matching queryKey
+
 ## `queryClient.invalidateQueries`
 
 The `invalidateQueries` method can be used to invalidate and refetch single or multiple queries in the cache based on their query keys or any other functionally accessible property/state of the query. By default, all matching queries are immediately marked as invalid and active queries are refetched in the background.
diff --git a/docs/src/pages/reference/useQuery.md b/docs/src/pages/reference/useQuery.md
index a0f67cfbf..d62f8bd73 100644
--- a/docs/src/pages/reference/useQuery.md
+++ b/docs/src/pages/reference/useQuery.md
@@ -216,7 +216,7 @@ const result = useQuery({
   - Will be `true` if the query has been fetched after the component mounted.
   - This property can be used to not show any previously cached data.
 - `isFetching: boolean`
-  - Defaults to `true` so long as `enabled` is set to `false`
+  - Is `true` whenever a request is in-flight, which includes initial `loading` as well as background refetches.
   - Will be `true` if the query is currently fetching, including background fetching.
 - `failureCount: number`
   - The failure count for the query.
diff --git a/src/core/queryClient.ts b/src/core/queryClient.ts
index 4302250f7..4bed72139 100644
--- a/src/core/queryClient.ts
+++ b/src/core/queryClient.ts
@@ -123,6 +123,33 @@ export class QueryClient {
       .setData(updater, options)
   }
 
+  setQueriesData<TData>(
+    queryKey: QueryKey,
+    updater: Updater<TData | undefined, TData>,
+    options?: SetDataOptions
+  ): [QueryKey, TData][]
+
+  setQueriesData<TData>(
+    filters: QueryFilters,
+    updater: Updater<TData | undefined, TData>,
+    options?: SetDataOptions
+  ): [QueryKey, TData][]
+
+  setQueriesData<TData>(
+    queryKeyOrFilters: QueryKey | QueryFilters,
+    updater: Updater<TData | undefined, TData>,
+    options?: SetDataOptions
+  ): [QueryKey, TData][] {
+    return notifyManager.batch(() =>
+      this.getQueryCache()
+        .findAll(queryKeyOrFilters)
+        .map(({ queryKey }) => [
+          queryKey,
+          this.setQueryData<TData>(queryKey, updater, options),
+        ])
+    )
+  }
+
   getQueryState<TData = unknown, TError = undefined>(
     queryKey: QueryKey,
     filters?: QueryFilters
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index d8e8dd571..cc1cd3e85 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -507,13 +507,31 @@ export class QueryObserver<
       } else {
         placeholderData =
           typeof options.placeholderData === 'function'
-            ? (options.placeholderData as PlaceholderDataFunction<TData>)()
+            ? (options.placeholderData as PlaceholderDataFunction<TQueryData>)()
             : options.placeholderData
+        if (options.select && typeof placeholderData !== 'undefined') {
+          try {
+            placeholderData = options.select(placeholderData)
+            if (options.structuralSharing !== false) {
+              placeholderData = replaceEqualDeep(
+                prevResult?.data,
+                placeholderData
+              )
+            }
+            this.previousSelectError = null
+          } catch (selectError) {
+            getLogger().error(selectError)
+            error = selectError
+            this.previousSelectError = selectError
+            errorUpdatedAt = Date.now()
+            status = 'error'
+          }
+        }
       }
 
       if (typeof placeholderData !== 'undefined') {
         status = 'success'
-        data = placeholderData
+        data = placeholderData as TData
         isPlaceholderData = true
       }
     }
diff --git a/src/core/types.ts b/src/core/types.ts
index 4fd2e6bc8..ba3fafd52 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -184,7 +184,7 @@ export interface QueryObserverOptions<
   /**
    * If set, this value will be used as the placeholder data for this particular query observer while the query is still in the `loading` data and no initialData has been provided.
    */
-  placeholderData?: TData | PlaceholderDataFunction<TData>
+  placeholderData?: TQueryData | PlaceholderDataFunction<TQueryData>
   /**
    * If set, the observer will optimistically set the result in fetching state before the query has actually started fetching.
    * This is to make sure the results are not lagging behind.
diff --git a/src/devtools/devtools.tsx b/src/devtools/devtools.tsx
index 449567992..00fe99389 100644
--- a/src/devtools/devtools.tsx
+++ b/src/devtools/devtools.tsx
@@ -17,7 +17,7 @@ import {
   Select,
   ActiveQueryPanel,
 } from './styledComponents'
-import { ThemeProvider } from './theme'
+import { ThemeProvider, defaultTheme as theme } from './theme'
 import { getQueryStatusLabel, getQueryStatusColor } from './utils'
 import Explorer from './Explorer'
 import Logo from './Logo'
@@ -74,20 +74,6 @@ interface DevtoolsPanelOptions {
 
 const isServer = typeof window === 'undefined'
 
-const theme = {
-  background: '#0b1521',
-  backgroundAlt: '#132337',
-  foreground: 'white',
-  gray: '#3f4e60',
-  grayAlt: '#222e3e',
-  inputBackgroundColor: '#fff',
-  inputTextColor: '#000',
-  success: '#00ab52',
-  danger: '#ff0085',
-  active: '#006bff',
-  warning: '#ffb200',
-}
-
 export function ReactQueryDevtools({
   initialIsOpen,
   panelProps = {},
diff --git a/src/devtools/theme.tsx b/src/devtools/theme.tsx
index b6f6ab065..fca51d745 100644
--- a/src/devtools/theme.tsx
+++ b/src/devtools/theme.tsx
@@ -1,10 +1,26 @@
-// @ts-nocheck
-
 import React from 'react'
 
-const ThemeContext = React.createContext()
+export const defaultTheme = {
+  background: '#0b1521',
+  backgroundAlt: '#132337',
+  foreground: 'white',
+  gray: '#3f4e60',
+  grayAlt: '#222e3e',
+  inputBackgroundColor: '#fff',
+  inputTextColor: '#000',
+  success: '#00ab52',
+  danger: '#ff0085',
+  active: '#006bff',
+  warning: '#ffb200',
+}
+
+interface ProviderProps {
+  theme: typeof defaultTheme
+}
+
+const ThemeContext = React.createContext(defaultTheme)
 
-export function ThemeProvider({ theme, ...rest }) {
+export function ThemeProvider({ theme, ...rest }: ProviderProps) {
   return <ThemeContext.Provider value={theme} {...rest} />
 }
 
diff --git a/src/react/useBaseQuery.ts b/src/react/useBaseQuery.ts
index 9e60e3bbc..55f7b7c81 100644
--- a/src/react/useBaseQuery.ts
+++ b/src/react/useBaseQuery.ts
@@ -130,7 +130,8 @@ export function useBaseQuery<
   // Handle error boundary
   if (
     (defaultedOptions.suspense || defaultedOptions.useErrorBoundary) &&
-    result.isError
+    result.isError &&
+    !result.isFetching
   ) {
     throw result.error
   }


Test patch:
diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx
index 69afd7f56..0dca395e2 100644
--- a/src/core/tests/queryClient.test.tsx
+++ b/src/core/tests/queryClient.test.tsx
@@ -166,6 +166,44 @@ describe('queryClient', () => {
     })
   })
 
+  describe('setQueriesData', () => {
+    test('should update all existing, matching queries', () => {
+      queryClient.setQueryData(['key', 1], 1)
+      queryClient.setQueryData(['key', 2], 2)
+
+      const result = queryClient.setQueriesData<number>('key', old => old! + 5)
+
+      expect(result).toEqual([
+        [['key', 1], 6],
+        [['key', 2], 7],
+      ])
+      expect(queryClient.getQueryData(['key', 1])).toBe(6)
+      expect(queryClient.getQueryData(['key', 2])).toBe(7)
+    })
+
+    test('should accept queryFilters', () => {
+      queryClient.setQueryData(['key', 1], 1)
+      queryClient.setQueryData(['key', 2], 2)
+      const query1 = queryCache.find(['key', 1])!
+
+      const result = queryClient.setQueriesData<number>(
+        { predicate: query => query === query1 },
+        old => old! + 5
+      )
+
+      expect(result).toEqual([[['key', 1], 6]])
+      expect(queryClient.getQueryData(['key', 1])).toBe(6)
+      expect(queryClient.getQueryData(['key', 2])).toBe(2)
+    })
+
+    test('should not update non existing queries', () => {
+      const result = queryClient.setQueriesData<string>('key', 'data')
+
+      expect(result).toEqual([])
+      expect(queryClient.getQueryData('key')).toBe(undefined)
+    })
+  })
+
   describe('getQueryData', () => {
     test('should return the query data if the query is found', () => {
       const key = queryKey()
diff --git a/src/react/tests/QueryResetErrorBoundary.test.tsx b/src/react/tests/QueryResetErrorBoundary.test.tsx
index 59c2094e3..64ef3015b 100644
--- a/src/react/tests/QueryResetErrorBoundary.test.tsx
+++ b/src/react/tests/QueryResetErrorBoundary.test.tsx
@@ -131,6 +131,66 @@ describe('QueryErrorResetBoundary', () => {
     consoleMock.mockRestore()
   })
 
+  it('should retry fetch if the reset error boundary has been reset and the query contains data from a previous fetch', async () => {
+    const key = queryKey()
+
+    let succeed = false
+    const consoleMock = mockConsoleError()
+
+    function Page() {
+      const { data } = useQuery(
+        key,
+        async () => {
+          await sleep(10)
+          if (!succeed) {
+            throw new Error('Error')
+          } else {
+            return 'data'
+          }
+        },
+        {
+          retry: false,
+          useErrorBoundary: true,
+          initialData: 'initial',
+        }
+      )
+      return <div>{data}</div>
+    }
+
+    const rendered = renderWithClient(
+      queryClient,
+      <QueryErrorResetBoundary>
+        {({ reset }) => (
+          <ErrorBoundary
+            onReset={reset}
+            fallbackRender={({ resetErrorBoundary }) => (
+              <div>
+                <div>error boundary</div>
+                <button
+                  onClick={() => {
+                    resetErrorBoundary()
+                  }}
+                >
+                  retry
+                </button>
+              </div>
+            )}
+          >
+            <Page />
+          </ErrorBoundary>
+        )}
+      </QueryErrorResetBoundary>
+    )
+
+    await waitFor(() => rendered.getByText('error boundary'))
+    await waitFor(() => rendered.getByText('retry'))
+    succeed = true
+    fireEvent.click(rendered.getByText('retry'))
+    await waitFor(() => rendered.getByText('data'))
+
+    consoleMock.mockRestore()
+  })
+
   it('should not retry fetch if the reset error boundary has not been reset after a previous reset', async () => {
     const key = queryKey()
 
diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx
index 0b3a16f61..3c752b7e3 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -3462,6 +3462,95 @@ describe('useQuery', () => {
     ])
   })
 
+  it('placeholder data should run through select', async () => {
+    const key1 = queryKey()
+
+    const states: UseQueryResult<string>[] = []
+
+    function Page() {
+      const state = useQuery(key1, () => 1, {
+        placeholderData: 23,
+        select: data => String(data * 2),
+      })
+
+      states.push(state)
+
+      return (
+        <div>
+          <h2>Data: {state.data}</h2>
+          <div>Status: {state.status}</div>
+        </div>
+      )
+    }
+
+    const rendered = renderWithClient(queryClient, <Page />)
+    await waitFor(() => rendered.getByText('Data: 2'))
+
+    expect(states).toMatchObject([
+      {
+        isSuccess: true,
+        isPlaceholderData: true,
+        data: '46',
+      },
+      {
+        isSuccess: true,
+        isPlaceholderData: false,
+        data: '2',
+      },
+    ])
+  })
+
+  it('placeholder data function result should run through select', async () => {
+    const key1 = queryKey()
+
+    const states: UseQueryResult<string>[] = []
+    let placeholderFunctionRunCount = 0
+
+    function Page() {
+      const state = useQuery(key1, () => 1, {
+        placeholderData: () => {
+          placeholderFunctionRunCount++
+          return 23
+        },
+        select: data => String(data * 2),
+      })
+
+      states.push(state)
+
+      return (
+        <div>
+          <h2>Data: {state.data}</h2>
+          <div>Status: {state.status}</div>
+        </div>
+      )
+    }
+
+    const rendered = renderWithClient(queryClient, <Page />)
+    await waitFor(() => rendered.getByText('Data: 2'))
+
+    rendered.rerender(<Page />)
+
+    expect(states).toMatchObject([
+      {
+        isSuccess: true,
+        isPlaceholderData: true,
+        data: '46',
+      },
+      {
+        isSuccess: true,
+        isPlaceholderData: false,
+        data: '2',
+      },
+      {
+        isSuccess: true,
+        isPlaceholderData: false,
+        data: '2',
+      },
+    ])
+
+    expect(placeholderFunctionRunCount).toEqual(1)
+  })
+
   it('should cancel the query function when there are no more subscriptions', async () => {
     const key = queryKey()
     let cancelFn: jest.Mock = jest.fn()