---
id: ssr
title: SSR
---

React Query supports two ways of prefetching data on the server and passing that to the queryClient.

- Prefetch the data yourself and pass it in as `initialData`
  - Quick to set up for simple cases
  - Has some caveats
- Prefetch the query on the server, dehydrate the cache and rehydrate it on the client
  - Requires slightly more setup up front

## Using Next.js

The exact implementation of these mechanisms may vary from platform to platform, but we recommend starting with Next.js which supports [2 forms of pre-rendering](https://nextjs.org/docs/basic-features/data-fetching):

- Static Generation (SSG)
- Server-side Rendering (SSR)

React Query supports both of these forms of pre-rendering regardless of what platform you may be using

### Using `initialData`

Together with Next.js's [`getStaticProps`](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation) or [`getServerSideProps`](https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering), you can pass the data you fetch in either method to `useQuery`'s' `initialData` option. From React Query's perspective, these integrate in the same way, `getStaticProps` is shown below:

```js
export async function getStaticProps() {
  const posts = await getPosts()
  return { props: { posts } }
}

function Posts(props) {
  const { data } = useQuery('posts', getPosts, { initialData: props.posts })

  // ...
}
```

The setup is minimal and this can be a quick solution for some cases, but there are a **few tradeoffs to consider** when compared to the full approach:

- If you are calling `useQuery` in a component deeper down in the tree you need to pass the `initialData` down to that point
- If you are calling `useQuery` with the same query in multiple locations, you need to pass `initialData` to all of them
- There is no way to know at what time the query was fetched on the server, so `dataUpdatedAt` and determining if the query needs refetching is based on when the page loaded instead

### Using Hydration

React Query supports prefetching multiple queries on the server in Next.js and then _dehydrating_ those queries to the queryClient. This means the server can prerender markup that is immediately available on page load and as soon as JS is available, React Query can upgrade or _hydrate_ those queries with the full functionality of the library. This includes refetching those queries on the client if they have become stale since the time they were rendered on the server.

To support caching queries on the server and set up hydration:

- Create a new `QueryClient` instance
- Wrap your app component with `<QueryClientProvider>` and pass it the client instance
- Wrap your app component with `<Hydrate>` and pass it the `dehydratedState` prop from `pageProps`

```js
// _app.jsx
import { QueryClient, QueryClientProvider } from 'react-query'
import { Hydrate } from 'react-query/hydration'

const queryClient = new QueryClient()

export default function MyApp({ Component, pageProps }) {
  return (
    <QueryClientProvider client={queryClient}>
      <Hydrate state={pageProps.dehydratedState}>
        <Component {...pageProps} />
      </Hydrate>
    </QueryClientProvider>
  )
}
```

Now you are ready to prefetch some data in your pages with either [`getStaticProps`](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation) (for SSG) or [`getServerSideProps`](https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering) (for SSR). From React Query's perspective, these integrate in the same way, `getStaticProps` is shown below.

- Create a new `QueryClient` instance for each page request
- Prefetch the data using the clients `prefetchQuery` method and wait for it to complete
- Use `dehydrate` to dehydrate the query cache and pass it to the page via the `dehydratedState` prop. This is the same prop that the cache will be picked up from in your `_app.js`

```js
// pages/posts.jsx
import { QueryClient, useQuery } from 'react-query'
import { dehydrate } from 'react-query/hydration'

export async function getStaticProps() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery('posts', getPosts)

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  }
}

function Posts() {
  // This useQuery could just as well happen in some deeper child to
  // the "Posts"-page, data will be available immediately either way
  const { data } = useQuery('posts', getPosts)

  // This query was not prefetched on the server and will not start
  // fetching until on the client, both patterns are fine to mix
  const { data: otherData } = useQuery('posts-2', getPosts)

  // ...
}
```

As demonstrated, it's fine to prefetch some queries and let others fetch on the queryClient. This means you can control what content server renders or not by adding or removing `prefetchQuery` for a specific query.

## Using Other Frameworks or Custom SSR Frameworks

This guide is at-best, a high level overview of how SSR with React Query should work. Your mileage may vary since there are many different possible setups for SSR.

> If you can, please contribution your findings back to this page for any framework specific guidance!

### On the Server

- Create a new `QueryClient` instance
- Using the client, prefetch any data you need
- Dehydrate the client
- Render your app with the client provider and also **using the dehydrated state. This is extremely important! You must render both server and client using the same dehydrated state to ensure hydration on the client produces the exact same markup as the server.**
- Serialize and embed the dehydrated cache to be sent to the client with the HTML

> 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)

```js
import { QueryClient, QueryClientProvider } from 'react-query'
import { dehydrate, Hydrate } from 'react-query/hydration'

const queryClient = new QueryClient()
await queryClient.prefetchQuery('key', fn)
const dehydratedState = dehydrate(queryClient)

const html = ReactDOM.renderToString(
  <QueryClientProvider client={queryClient}>
    <Hydrate state={dehydratedState}>
      <App />
    </Hydrate>
  </QueryClientProvider>
)

res.send(`
  <html>
    <body>
      <div id="root">${html}</div>
      <script>
        window.__REACT_QUERY_STATE__ = ${JSON.stringify(dehydratedState)};
      </script>
    </body>
  </html>
  `)
```

### Client

- Parse the dehydrated cache state that was sent to the client with the HTML
- Create a new `QueryClient` instance
- Render your app with the client provider and also **using the dehydrated state. This is extremely important! You must render both server and client using the same dehydrated state to ensure hydration on the client produces the exact same markup as the server.**

```js
import { QueryClient, QueryClientProvider } from 'react-query'
import { Hydrate } from 'react-query/hydration'

const dehydratedState = window.__REACT_QUERY_STATE__

const queryClient = new QueryClient()

ReactDOM.hydrate(
  <QueryClientProvider client={queryClient}>
    <Hydrate state={dehydratedState}>
      <App />
    </Hydrate>
  </QueryClientProvider>,
  document.getElementById('root')
)
```

## Tips, Tricks and Caveats

### Only successful queries are included in dehydration

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 queryClient. 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, use `fetchQuery` and catch any errors to handle those manually.

### Staleness is measured from when the query was fetched on the server

A query is considered stale depending on when it was `dataUpdatedAt`. A caveat here is that the server needs to have the correct time for this to work properly, but UTC time is used, so timezones do not factor into this.

Because `staleTime` defaults to `0`, queries will be refetched in the background on page load by default. You might want to use a higher `staleTime` to avoid this double fetching, especially if you don't cache your markup.

This refetching of stale queries is a perfect match when caching markup in a CDN! You can set the cache time of the page itself decently high to avoid having to re-render pages on the server, but configure the `staleTime` of the queries lower to make sure data is refetched in the background as soon as a user visits the page. Maybe you want to cache the pages for a week, but refetch the data automatically on page load if it's older than a day?


"build": "rescripts build",
"test": "rescripts test",
"eject": "rescripts eject"
},
"dependencies": {
"axios": "0.19.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-query": "^3.5.0",
"react-scripts": "3.0.1",

keep keep keep keep keep replace keep keep keep keep


keep keep keep keep replace keep keep keep keep keep


.previewAlertContainer {
  display: flex;
  justify-content: center;
}

.previewAlert {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  text-align: center;
  border: 1px solid #eaeaea;
  width: 400px;
  padding: 16px;
  border-radius: 5px;
}

.escapePreview {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border: none;
  background-color: black;
  border: 1px solid black;
  color: white;
  padding: 10px;
  height: 24px;
  border-radius: 5px;
  transition: all 0.2s ease 0s;
}
.escapePreview:hover {
  background-color: white;
  color: black;
  border: 1px solid black;
  cursor: pointer;
}

.titleContainer {
  display: inline-flex;
  align-items: center;
  justify-content: flex-start;
}
.draftBadge {
  border-radius: 16px;
  background-color: black;
  color: white;
  font-size: 14px;
  font-weight: 200;
  padding: 2px 7px;
  margin-right: 6px;
}

.noPosts {
  text-align: center;
}


# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

yarn.lock
package-lock.json

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*


---
id: suspense
title: Suspense
toc: false
---

- [Open in CodeSandbox](https://codesandbox.io/s/github/tannerlinsley/react-query/tree/master/examples/suspense)
- [View Source](https://github.com/tannerlinsley/react-query/tree/master/examples/suspense)

<iframe
  src="https://codesandbox.io/embed/github/tannerlinsley/react-query/tree/master/examples/suspense?autoresize=1&fontsize=14&theme=dark"
  title="tannerlinsley/react-query: suspense"
  sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
  style={{
    width: '100%',
    height: '80vh',
    border: '0',
    borderRadius: 8,
    overflow: 'hidden',
    position: 'static',
    zIndex: 0,
  }}
></iframe>


keep keep keep replace keep keep keep keep


import React from 'react'

import { notifyManager } from '../core/notifyManager'
import { QueryObserver } from '../core/queryObserver'
import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary'
import { useQueryClient } from './QueryClientProvider'
import { UseBaseQueryOptions } from './types'

export function useBaseQuery<TQueryFnData, TError, TData, TQueryData>(
  options: UseBaseQueryOptions<TQueryFnData, TError, TData, TQueryData>,
  Observer: typeof QueryObserver
) {
  const queryClient = useQueryClient()
  const errorResetBoundary = useQueryErrorResetBoundary()
  const defaultedOptions = queryClient.defaultQueryObserverOptions(options)

  // Include callbacks in batch renders
  if (defaultedOptions.onError) {
    defaultedOptions.onError = notifyManager.batchCalls(
      defaultedOptions.onError
    )
  }

  if (defaultedOptions.onSuccess) {
    defaultedOptions.onSuccess = notifyManager.batchCalls(
      defaultedOptions.onSuccess
    )
  }

  if (defaultedOptions.onSettled) {
    defaultedOptions.onSettled = notifyManager.batchCalls(
      defaultedOptions.onSettled
    )
  }

  if (defaultedOptions.suspense) {
    // Always set stale time when using suspense to prevent
    // fetching again when directly re-mounting after suspense
    if (typeof defaultedOptions.staleTime !== 'number') {
      defaultedOptions.staleTime = 1000
    }

    // Prevent retrying failed query if the error boundary has not been reset yet
    if (!errorResetBoundary.isReset()) {
      defaultedOptions.retryOnMount = false
    }
  }

  // Create query observer
  const observerRef = React.useRef<QueryObserver<any, any, any, any>>()
  const observer =
    observerRef.current || new Observer(queryClient, defaultedOptions)
  observerRef.current = observer

  // Update options
  if (observer.hasListeners()) {
    observer.setOptions(defaultedOptions)
  }

  const currentResult = observer.getCurrentResult()
  const [, setCurrentResult] = React.useState(currentResult)

  // Subscribe to the observer
  React.useEffect(() => {
    errorResetBoundary.clearReset()
    return observer.subscribe(notifyManager.batchCalls(setCurrentResult))
  }, [observer, errorResetBoundary])

  // Handle suspense
  if (observer.options.suspense || observer.options.useErrorBoundary) {
    if (observer.options.suspense && currentResult.isLoading) {
      errorResetBoundary.clearReset()
      const unsubscribe = observer.subscribe()
      throw observer.refetch().finally(unsubscribe)
    }

    if (currentResult.isError) {
      throw currentResult.error
    }
  }

  return observer.options.notifyOnChangeProps === 'tracked'
    ? observer.getTrackedCurrentResult()
    : currentResult
}


import React from 'react'

import { notifyManager } from '../core/notifyManager'
import { QueriesObserver } from '../core/queriesObserver'
import { useQueryClient } from './QueryClientProvider'
import { UseQueryOptions, UseQueryResult } from './types'

export function useQueries(queries: UseQueryOptions[]): UseQueryResult[] {
  const queryClient = useQueryClient()

  // Create queries observer
  const observerRef = React.useRef<QueriesObserver>()
  const observer =
    observerRef.current || new QueriesObserver(queryClient, queries)
  observerRef.current = observer

  // Update queries
  if (observer.hasListeners()) {
    observer.setQueries(queries)
  }

  const [currentResult, setCurrentResult] = React.useState(() =>
    observer.getCurrentResult()
  )

  // Subscribe to the observer
  React.useEffect(
    () => observer.subscribe(notifyManager.batchCalls(setCurrentResult)),
    [observer]
  )

  return currentResult
}


---
id: useQueryClient
title: useQueryClient
---

The `useQueryClient` hook returns the current `QueryClient` instance.

```js
import { useQueryClient } from 'react-query'

const queryClient = useQueryClient()
```


keep keep keep keep replace keep keep keep keep


# Example

To run this example:

- `npm install` or `yarn`
- `npm run start` or `yarn start`


---
id: caching
title: Caching Examples
---

> Please thoroughly read the [Important Defaults](./important-defaults) before reading this guide

## Basic Example

This caching example illustrates the story and lifecycle of:

- Query Instances with and without cache data
- Background Refetching
- Inactive Queries
- Garbage Collection

Let's assume we are using the default `cacheTime` of **5 minutes** and the default `staleTime` of `0`.

- A new instance of `useQuery('todos', fetchTodos)` mounts.
  - Since no other queries have been made with this query + variable combination, this query will show a hard loading state and make a network request to fetch the data.
  - It will then cache the data using `'todos'` and `fetchTodos` as the unique identifiers for that cache.
  - The hook will mark itself as stale after the configured `staleTime` (defaults to `0`, or immediately).
- A second instance of `useQuery('todos', fetchTodos)` mounts elsewhere.
  - Because this exact data exist in the cache from the first instance of this query, that data is immediately returned from the cache.
- A background refetch is triggered for both queries (but only one request), since a new instance appeared on screen.
  - Both instances are updated with the new data if the fetch is successful
- Both instances of the `useQuery('todos', fetchTodos)` query are unmounted and no longer in use.
  - Since there are no more active instances to this query, a cache timeout is set using `cacheTime` to delete and garbage collect the query (defaults to **5 minutes**).
- Before the cache timeout has completed another instance of `useQuery('todos', fetchTodos)` mounts. The query immediately returns the available cached value while the `fetchTodos` function is being run in the background to populate the query with a fresh value.
- The final instance of `useQuery('todos', fetchTodos)` unmounts.
- No more instances of `useQuery('todos', fetchTodos)` appear within **5 minutes**.
  - This query and its data are deleted and garbage collected.


const items = []

export default async (req, res) => {
  await new Promise(r => setTimeout(r, 1000))

  if (req.method === 'POST') {
    const { text } = req.body

    // sometimes it will fail, this will cause a regression on the UI

    if (Math.random() > 0.7) {
      res.status(500)
      res.json({ message: 'Could not add item!' })
      return
    }

    const newTodo = { id: Math.random().toString(), text: text.toUpperCase() }
    items.push(newTodo)
    res.json(newTodo)
    return
  } else {
    res.json({
      ts: Date.now(),
      items,
    })
  }
}


---
id: optimistic-updates
title: Optimistic Updates
---

When you optimistically update your state before performing a mutation, there is a non-zero chance that the mutation will fail. In most cases, you can just trigger a refetch for your optimistic queries to revert them to their true server state. In some circumstances though, refetching may not work correctly and the mutation error could represent some type of server issue that won't make it possible to refetch. In this event, you can instead choose to rollback your update.

To do this, `useMutation`'s `onMutate` handler option allows you to return a value that will later be passed to both `onError` and `onSettled` handlers as the last argument. In most cases, it is most useful to pass a rollback function.

## Updating a list of todos when adding a new todo

```js
const queryClient = useQueryClient()

useMutation(updateTodo, {
  // When mutate is called:
  onMutate: async newTodo => {
    // Cancel any outgoing refetches (so they don't overwrite our optimistic update)
    await queryClient.cancelQueries('todos')

    // Snapshot the previous value
    const previousTodos = queryClient.getQueryData('todos')

    // Optimistically update to the new value
    queryClient.setQueryData('todos', old => [...old, newTodo])

    // Return a context object with the snapshotted value
    return { previousTodos }
  },
  // If the mutation fails, use the context returned from onMutate to roll back
  onError: (err, newTodo, context) => {
    queryClient.setQueryData('todos', context.previousTodos)
  },
  // Always refetch after error or success:
  onSettled: () => {
    queryClient.invalidateQueries('todos')
  },
})
```

## Updating a single todo

```js
useMutation(updateTodo, {
  // When mutate is called:
  onMutate: async newTodo => {
    // Cancel any outgoing refetches (so they don't overwrite our optimistic update)
    await queryClient.cancelQueries(['todos', newTodo.id])

    // Snapshot the previous value
    const previousTodo = queryClient.getQueryData(['todos', newTodo.id])

    // Optimistically update to the new value
    queryClient.setQueryData(['todos', newTodo.id], newTodo)

    // Return a context with the previous and new todo
    return { previousTodo, newTodo }
  },
  // If the mutation fails, use the context we returned above
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(
      ['todos', context.newTodo.id],
      context.previousTodo
    )
  },
  // Always refetch after error or success:
  onSettled: newTodo => {
    queryClient.invalidateQueries(['todos', newTodo.id])
  },
})
```

You can also use the `onSettled` function in place of the separate `onError` and `onSuccess` handlers if you wish:

```js
useMutation(updateTodo, {
  // ...
  onSettled: (newTodo, error, variables, context) => {
    if (error) {
      // do something
    }
  },
})
```


---
id: background-fetching-indicators
title: Background Fetching Indicators
---

A query's `status === 'loading'` state is sufficient enough to show the initial hard-loading state for a query, but sometimes you may want to display an additional indicator that a query is refetching in the background. To do this, queries also supply you with an `isFetching` boolean that you can use to show that it's in a fetching state, regardless of the state of the `status` variable:

```js
function Todos() {
  const { status, data: todos, error, isFetching } = useQuery(
    'todos',
    fetchTodos
  )

  return status === 'loading' ? (
    <span>Loading...</span>
  ) : status === 'error' ? (
    <span>Error: {error.message}</span>
  ) : (
    <>
      {isFetching ? <div>Refreshing...</div> : null}

      <div>
        {todos.map(todo => (
          <Todo todo={todo} />
        ))}
      </div>
    </>
  )
}
```

# Displaying Global Background Fetching Loading State

In addition to individual query loading states, if you would like to show a global loading indicator when **any** queries are fetching (including in the background), you can use the `useIsFetching` hook:

```js
import { useIsFetching } from 'react-query'

function GlobalLoadingIndicator() {
  const isFetching = useIsFetching()

  return isFetching ? (
    <div>Queries are fetching in the background...</div>
  ) : null
}
```


import { hasEqualOptions, parseOptions } from './utils'

if (typeof window !== 'undefined') {
  require('intersection-observer')
}

const manager = (function makeManager() {
  const observers = new Map()

  function getObserver(options) {
    return (
      findObserver(options) ||
      new IntersectionObserver(intersectionCallback, options)
    )
  }

  function findObserver(options = {}) {
    const parsedOptions = parseOptions(options)
    for (const observer of observers.keys()) {
      if (hasEqualOptions(observer, parsedOptions)) {
        return observer
      }
    }
    return null
  }

  function getObserverTargets(observer) {
    return !observers.has(observer)
      ? observers.set(observer, new Map()).get(observer)
      : observers.get(observer)
  }

  function observeTarget(observer, target, handler) {
    const targets = getObserverTargets(observer)
    targets.set(target, handler)
    observer.observe(target)
  }

  function unobserveTarget(observer, target) {
    const handlers = getObserverTargets(observer)
    handlers.delete(target)
    observer.unobserve(target)
  }

  function intersectionCallback(entries, observer) {
    for (let entry of entries) {
      const handlers = getObserverTargets(observer)
      const handler = handlers.get(entry.target)
      if (handler) {
        handler(entry)
      }
    }
  }

  return {
    getObserver,
    observeTarget,
    unobserveTarget
  }
})()

export default manager
export const { getObserver } = manager
export const { observeTarget } = manager
export const { unobserveTarget } = manager


keep keep keep keep keep replace keep keep keep keep keep

new

>0.2%
not dead
not ie <= 11
not op_mini all


---
id: custom-hooks
title: Custom Hooks
toc: false
---

- [Open in CodeSandbox](https://codesandbox.io/s/github/tannerlinsley/react-query/tree/master/examples/custom-hooks)
- [View Source](https://github.com/tannerlinsley/react-query/tree/master/examples/custom-hooks)

<iframe
  src="https://codesandbox.io/embed/github/tannerlinsley/react-query/tree/master/examples/custom-hooks?autoresize=1&fontsize=14&theme=dark"
  title="tannerlinsley/react-query: custom-hooks"
  sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
  style={{
    width: '100%',
    height: '80vh',
    border: '0',
    borderRadius: 8,
    overflow: 'hidden',
    position: 'static',
    zIndex: 0,
  }}
></iframe>


import React from "react";
import { Typography, Link } from "@material-ui/core";
import { Link as RouterLink } from "react-router-dom";
import { useQuery } from "react-query";
import fetch from "./fetch";

export default function Characters() {
  const { status, data } = useQuery("characters", () =>
    fetch("https://rickandmortyapi.com/api/character/")
  );

  if (status === "loading") return
Loading...
;
  if (status === "error") return
Error :(
;

  console.info(data);

  return (

      <Typography variant="h2">Characters</Typography>
      {data.results.map(person => {
        return (

            <Link component={RouterLink} to={`/characters/${person.id}`}>
              <Typography variant="h6">
                {person.name} - {person.gender}: {person.species}
              </Typography>
            </Link>

        );
      })}

  );
}


last 1 chrome version
last 1 firefox version
last 1 safari version


export * from './Layout'
export * from './Header'
export * from './InfoBox'
export * from './PostList'


---
id: focus-refetching
title: Focus Refetching
toc: false
---

- [Open in CodeSandbox](https://codesandbox.io/s/github/tannerlinsley/react-query/tree/master/examples/focus-refetching)
- [View Source](https://github.com/tannerlinsley/react-query/tree/master/examples/focus-refetching)

<iframe
  src="https://codesandbox.io/embed/github/tannerlinsley/react-query/tree/master/examples/focus-refetching?autoresize=1&fontsize=14&theme=dark"
  title="tannerlinsley/react-query: focus-refetching"
  sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
  style={{
    width: '100%',
    height: '80vh',
    border: '0',
    borderRadius: 8,
    overflow: 'hidden',
    position: 'static',
    zIndex: 0,
  }}
></iframe>


import {
  QueryFilters,
  getQueryKeyHashFn,
  matchQuery,
  parseFilterArgs,
} from './utils'
import { Query, QueryState } from './query'
import type { QueryKey, QueryOptions } from './types'
import { notifyManager } from './notifyManager'
import type { QueryClient } from './queryClient'
import { Subscribable } from './subscribable'

// TYPES

interface QueryCacheConfig {
  onError?: (error: unknown, query: Query<unknown, unknown, unknown>) => void
}

interface QueryHashMap {
  [hash: string]: Query<any, any>
}

type QueryCacheListener = (query?: Query) => void

// CLASS

export class QueryCache extends Subscribable<QueryCacheListener> {
  config: QueryCacheConfig

  private queries: Query<any, any>[]
  private queriesMap: QueryHashMap

  constructor(config?: QueryCacheConfig) {
    super()
    this.config = config || {}
    this.queries = []
    this.queriesMap = {}
  }

  build<TQueryFnData, TError, TData>(
    client: QueryClient,
    options: QueryOptions<TQueryFnData, TError, TData>,
    state?: QueryState<TData, TError>
  ): Query<TQueryFnData, TError, TData> {
    const hashFn = getQueryKeyHashFn(options)
    const queryKey = options.queryKey!
    const queryHash = options.queryHash ?? hashFn(queryKey)
    let query = this.get<TQueryFnData, TError, TData>(queryHash)

    if (!query) {
      query = new Query({
        cache: this,
        queryKey,
        queryHash,
        options: client.defaultQueryOptions(options),
        state,
        defaultOptions: client.getQueryDefaults(queryKey),
      })
      this.add(query)
    }

    return query
  }

  add(query: Query<any, any>): void {
    if (!this.queriesMap[query.queryHash]) {
      this.queriesMap[query.queryHash] = query
      this.queries.push(query)
      this.notify(query)
    }
  }

  remove(query: Query<any, any>): void {
    const queryInMap = this.queriesMap[query.queryHash]

    if (queryInMap) {
      query.destroy()

      this.queries = this.queries.filter(x => x !== query)

      if (queryInMap === query) {
        delete this.queriesMap[query.queryHash]
      }

      this.notify(query)
    }
  }

  clear(): void {
    notifyManager.batch(() => {
      this.queries.forEach(query => {
        this.remove(query)
      })
    })
  }

  get<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData>(
    queryHash: string
  ): Query<TQueryFnData, TError, TData> | undefined {
    return this.queriesMap[queryHash]
  }

  getAll(): Query[] {
    return this.queries
  }

  find<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData>(
    arg1: QueryKey,
    arg2?: QueryFilters
  ): Query<TQueryFnData, TError, TData> | undefined {
    const [filters] = parseFilterArgs(arg1, arg2)
    return this.queries.find(query => matchQuery(filters, query))
  }

  findAll(queryKey?: QueryKey, filters?: QueryFilters): Query[]
  findAll(filters?: QueryFilters): Query[]
  findAll(arg1?: QueryKey | QueryFilters, arg2?: QueryFilters): Query[]
  findAll(arg1?: QueryKey | QueryFilters, arg2?: QueryFilters): Query[] {
    const [filters] = parseFilterArgs(arg1, arg2)
    return filters
      ? this.queries.filter(query => matchQuery(filters, query))
      : this.queries
  }

  notify(query?: Query<any, any>) {
    notifyManager.batch(() => {
      this.listeners.forEach(listener => {
        listener(query)
      })
    })
  }

  onFocus(): void {
    notifyManager.batch(() => {
      this.queries.forEach(query => {
        query.onFocus()
      })
    })
  }

  onOnline(): void {
    notifyManager.batch(() => {
      this.queries.forEach(query => {
        query.onOnline()
      })
    })
  }
}


import { useState, useCallback, useEffect } from 'react'

const useMediaQuery = width => {
  const [targetReached, setTargetReached] = useState(false)
  const updateTarget = useCallback(e => {
    if (e.matches) {
      setTargetReached(true)
    } else {
      setTargetReached(false)
    }
  }, [])
  useEffect(() => {
    const media = window.matchMedia(`(max-width: ${width}px)`)
    media.addListener(updateTarget) // Check on mount (callback is not called until a change occurs)

    if (media.matches) {
      setTargetReached(true)
    }

    return () => media.removeListener(updateTarget)
  }, [])
  return targetReached
}

const useIsMobile = () => {
  return useMediaQuery(1024)
}

export { useMediaQuery, useIsMobile }


import React from "react";
import { Typography } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import { Link } from "@material-ui/core";
import { Link as RouterLink } from "react-router-dom";

export default function Home(props) {
  const classes = useStyles();

  return (

      <Typography variant="h2">React Query Demo</Typography>
      <Typography variant="subtitle1">Using the Star Wars API</Typography>
      <Typography variant="subtitle2">
        (Built by @Brent_m_Clark
        )
      </Typography>

        <Typography variant="h5">Why React Query?</Typography>
        <Typography variant="body1">
          In this demo you will be able to see how React Query is a significant
          improvement over redux, mobx, and
          any other general-purpose state container.
        </Typography>
        <Typography variant="body1">
          No reducers, thunks, or sagas. No ES6 models to maintain in order to
          tag them as observable.
        </Typography>
        <Typography variant="body1">
          Simply associate a key with your fetch call and let{" "}
          React Query handle the rest.
        </Typography>
        <Typography variant="h5">Ready to get started?</Typography>
        <Typography variant="body1">
          Check out the{" "}
          <Link component={RouterLink} to="/films">
            Films
          </Link>{" "}
          and{" "}
          <Link component={RouterLink} to="/characters">
            Characters
          </Link>
          !
        </Typography>

  );
}

const useStyles = makeStyles(theme => ({
  main: {
    margin: "44px 0",
    "& p": {
      margin: "12px 0 24px"
    }
  }
}));


# See https://help.github.com/ignore-files/ for more about ignoring files.

# dependencies
node_modules

# builds
types
build
dist
lib
es
artifacts
.rpt2_cache
coverage
*.tgz

# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.next

npm-debug.log*
yarn-debug.log*
yarn-error.log*
.history
size-plugin.json
stats-hydration.json
stats-react.json
stats.html
.vscode/settings.json


"test": "rescripts test",
"eject": "rescripts eject"
},
"dependencies": {
"axios": "0.19.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-query": "^3.5.0",
"react-scripts": "3.0.1",


import * as React from 'react';
export const SidebarHeading = ({
  title,
  children
}) => {
  return (//

    //
{title}

    //
{children}

    //

{title}

{children}

  );
};
SidebarHeading.displayName = 'SidebarHeading';


{
  "extends": ["react-app", "prettier"],
  "rules": {
    // "eqeqeq": 0,
    // "jsx-a11y/anchor-is-valid": 0
  }
}


---
id: QueryClient
title: QueryClient
---

## `QueryClient`

The `QueryClient` can be used to interact with a cache:

```js
import { QueryClient } from 'react-query'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: Infinity,
    },
  },
})

await queryClient.prefetchQuery('posts', fetchPosts)
```

Its available methods are:

- [`fetchQuery`](#queryclientfetchquery)
- [`fetchInfiniteQuery`](#queryclientfetchinfinitequery)
- [`prefetchQuery`](#queryclientprefetchquery)
- [`prefetchInfiniteQuery`](#queryclientprefetchinfinitequery)
- [`getQueryData`](#queryclientgetquerydata)
- [`setQueryData`](#queryclientsetquerydata)
- [`getQueryState`](#queryclientgetquerystate)
- [`invalidateQueries`](#queryclientinvalidatequeries)
- [`refetchQueries`](#queryclientrefetchqueries)
- [`cancelQueries`](#queryclientcancelqueries)
- [`removeQueries`](#queryclientremovequeries)
- [`resetQueries`](#queryclientresetqueries)
- [`isFetching`](#queryclientisfetching)
- [`getDefaultOptions`](#queryclientsetdefaultoptions)
- [`setDefaultOptions`](#queryclientgetdefaultoptions)
- [`getQueryDefaults`](#queryclientgetquerydefaults)
- [`setQueryDefaults`](#queryclientsetquerydefaults)
- [`getMutationDefaults`](#queryclientgetmutationdefaults)
- [`setMutationDefaults`](#queryclientsetmutationdefaults)
- [`getQueryCache`](#queryclientgetquerycache)
- [`getMutationCache`](#queryclientgetmutationcache)
- [`clear`](#queryclientclear)

**Options**

- `queryCache?: QueryCache`
  - Optional
  - The query cache this client is connected to.
- `mutationCache?: MutationCache`
  - Optional
  - The mutation cache this client is connected to.
- `defaultOptions?: DefaultOptions`
  - Optional
  - Define defaults for all queries and mutations using this queryClient.

## `queryClient.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. Use the `prefetchQuery` method if you just want to fetch a query without needing the result.

If the query exists and the data is not invalidated or older than the given `staleTime`, then the data from the cache will be returned. Otherwise it will try to fetch the latest data.

> The difference between using `fetchQuery` and `setQueryData` is that `fetchQuery` 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
try {
  const data = await queryClient.fetchQuery(queryKey, queryFn)
} catch (error) {
  console.log(error)
}
```

Specify a `staleTime` to only fetch when the data is older than a certain amount of time:

```js
try {
  const data = await queryClient.fetchQuery(queryKey, queryFn, {
    staleTime: 10000,
  })
} catch (error) {
  console.log(error)
}
```

**Options**

The options for `fetchQuery` are exactly the same as those of [`useQuery`](./useQuery), except the following: `enabled, refetchInterval, refetchIntervalInBackground, refetchOnWindowFocus, refetchOnReconnect, notifyOnChangeProps, notifyOnChangePropsExclusions, onSuccess, onError, onSettled, useErrorBoundary, select, suspense, keepPreviousData, placeholderData`; which are stictly for useQuery and useInfiniteQuery. You can check the [source code](https://github.com/tannerlinsley/react-query/blob/361935a12cec6f36d0bd6ba12e84136c405047c5/src/core/types.ts#L83) for more clarity.

**Returns**

- `Promise<TData>`

## `queryClient.fetchInfiniteQuery`

`fetchInfiniteQuery` is similar to `fetchQuery` but can be used to fetch and cache an infinite query.

```js
try {
  const data = await queryClient.fetchInfiniteQuery(queryKey, queryFn)
  console.log(data.pages)
} catch (error) {
  console.log(error)
}
```

**Options**

The options for `fetchInfiniteQuery` are exactly the same as those of [`fetchQuery`](#queryclientfetchquery).

**Returns**

- `Promise<InfiniteData<TData>>`

## `queryClient.prefetchQuery`

`prefetchQuery` is an asynchronous method that can be used to prefetch a query before it is needed or rendered with `useQuery` and friends. The method works the same as `fetchQuery` except that is will not throw or return any data.

```js
await queryClient.prefetchQuery(queryKey, queryFn)
```

You can even use it with a default queryFn in your config!

```js
await queryClient.prefetchQuery(queryKey)
```

**Options**

The options for `prefetchQuery` are exactly the same as those of [`fetchQuery`](#queryclientfetchquery).

**Returns**

- `Promise<void>`
  - A promise is returned that will either immediately resolve if no fetch is needed or after the query has been executed. It will not return any data or throw any errors.

## `queryClient.prefetchInfiniteQuery`

`prefetchInfiniteQuery` is similar to `prefetchQuery` but can be used to prefetch and cache an infinite query.

```js
await queryClient.prefetchInfiniteQuery(queryKey, queryFn)
```

**Options**

The options for `prefetchInfiniteQuery` are exactly the same as those of [`fetchQuery`](#queryclientfetchquery).

**Returns**

- `Promise<void>`
  - A promise is returned that will either immediately resolve if no fetch is needed or after the query has been executed. It will not return any data or throw any errors.

## `queryClient.getQueryData`

`getQueryData` is a synchronous function that can be used to get an existing query's cached data. If the query does not exist, `undefined` will be returned.

```js
const data = queryClient.getQueryData(queryKey)
```

**Options**

- `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)

**Returns**

- `data: TData | undefined`
  - The data for the cached query, or `undefined` if the query does not exist.

## `queryClient.setQueryData`

`setQueryData` is a synchronous function that can be used to immediately update a query's cached data. If the query does not exist, it will be created. **If the query is not utilized by a query hook in the default `cacheTime` of 5 minutes, the query will be garbage collected**.

> The difference between using `setQueryData` and `fetchQuery` is that `setQueryData` is sync and assumes that you already synchronously have the data available. If you need to fetch the data asynchronously, it's suggested that you either refetch the query key or use `fetchQuery` to handle the asynchronous fetch.

```js
queryClient.setQueryData(queryKey, updater)
```

**Options**

- `queryKey: QueryKey`: [Query Keys](../guides/query-keys)
- `updater: unknown | (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.

**Using an updater value**

```js
setQueryData(queryKey, newData)
```

**Using an updater function**

For convenience in syntax, you can also pass an updater function which receives the current data value and returns the new one:

```js
setQueryData(queryKey, oldData => newData)
```

## `queryClient.getQueryState`

`getQueryState` is a synchronous function that can be used to get an existing query's state. If the query does not exist, `undefined` will be returned.

```js
const state = queryClient.getQueryState(queryKey)
console.log(state.dataUpdatedAt)
```

**Options**

- `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)

## `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.

- If you **do not want active queries to refetch**, and simply be marked as invalid, you can use the `refetchActive: false` option.
- If you **want inactive queries to refetch** as well, use the `refetchInactive: true` option

```js
await queryClient.invalidateQueries('posts', {
  exact,
  refetchActive: true,
  refetchInactive: false
}, { throwOnError })
```

**Options**

- `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)
  - `refetchActive: Boolean`
    - Defaults to `true`
    - When set to `false`, queries that match the refetch predicate and are actively being rendered via `useQuery` and friends will NOT be refetched in the background, and only marked as invalid.
  - `refetchInactive: Boolean`
    - Defaults to `false`
    - When set to `true`, queries that match the refetch predicate and are not being rendered via `useQuery` and friends will be both marked as invalid and also refetched in the background
- `refetchOptions?: RefetchOptions`:
  - `throwOnError?: boolean`
    - When set to `true`, this method will throw if any of the query refetch tasks fail.

## `queryClient.refetchQueries`

The `refetchQueries` method can be used to refetch queries based on certain conditions.

Examples:

```js
// refetch all queries:
await queryClient.refetchQueries()

// refetch all stale queries:
await queryClient.refetchQueries({ stale: true })

// refetch all active queries partially matching a query key:
await queryClient.refetchQueries(['posts'], { active: true })

// refetch all active queries exactly matching a query key:
await queryClient.refetchQueries(['posts', 1], { active: true, exact: true })
```

**Options**

- `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)
- `refetchOptions?: RefetchOptions`:
  - `throwOnError?: boolean`
    - When set to `true`, this method will throw if any of the query refetch tasks fail.

**Returns**

This function returns a promise that will resolve when all of the queries are done being refetched. By default, it **will not** throw an error if any of those queries refetches fail, but this can be configured by setting the `throwOnError` option to `true`

## `queryClient.cancelQueries`

The `cancelQueries` method can be used to cancel outgoing queries based on their query keys or any other functionally accessible property/state of the query.

This is most useful when performing optimistic updates since you will likely need to cancel any outgoing query refetches so they don't clobber your optimistic update when they resolve.

```js
await queryClient.cancelQueries('posts', { exact: true })
```

**Options**

- `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)

**Returns**

This method does not return anything

## `queryClient.removeQueries`

The `removeQueries` method can be used to remove queries from the cache based on their query keys or any other functionally accessible property/state of the query.

```js
queryClient.removeQueries(queryKey, { exact: true })
```

**Options**

- `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)

**Returns**

This method does not return anything

## `queryClient.resetQueries`

The `resetQueries` method can be used to reset queries in the cache to their
initial state based on their query keys or any other functionally accessible
property/state of the query.

This will notify subscribers — unlike `clear`, which removes all
subscribers — and reset the query to its pre-loaded state — unlike
`invalidateQueries`. If a query has `initialData`, the query's data will be
reset to that. If a query is active, it will be refetched.

```js
queryClient.resetQueries(queryKey, { exact: true })
```

**Options**

- `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)
- `resetOptions?: ResetOptions`:
  - `throwOnError?: boolean`
    - When set to `true`, this method will throw if any of the query refetch tasks fail.

**Returns**

This method returns a promise that resolves when all active queries have been refetched.

## `queryClient.isFetching`

This `isFetching` method returns an `integer` representing how many queries, if any, in the cache are currently fetching (including background-fetching, loading new pages, or loading more infinite query results)

```js
if (queryClient.isFetching()) {
  console.log('At least one query is fetching!')
}
```

React Query also exports a handy [`useIsFetching`](#useisfetching) hook that will let you subscribe to this state in your components without creating a manual subscription to the query cache.

**Options**

- `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)

**Returns**

This method returns the number of fetching queries.

## `queryClient.getDefaultOptions`

The `getDefaultOptions` method returns the default options which have been set when creating the client or with `setDefaultOptions`.

```js
const defaultOptions = queryClient.getDefaultOptions()
```

## `queryClient.setDefaultOptions`

The `setDefaultOptions` method can be used to dynamically set the default options for this queryClient.

```js
queryClient.setDefaultOptions({
  queries: {
    staleTime: Infinity,
  },
})
```

## `queryClient.getQueryDefaults`

The `getQueryDefaults` method returns the default options which have been set for specific queries:

```js
const defaultOptions = queryClient.getQueryDefaults('posts')
```

## `queryClient.setQueryDefaults`

`setQueryDefaults` can be used to set default options for specific queries:

```js
queryClient.setQueryDefaults('posts', { queryFn: fetchPosts })

function Component() {
  const { data } = useQuery('posts')
}
```

**Options**

- `queryKey: QueryKey`: [Query Keys](../guides/query-keys)
- `options: QueryOptions`

## `queryClient.getMutationDefaults`

The `getMutationDefaults` method returns the default options which have been set for specific mutations:

```js
const defaultOptions = queryClient.getMutationDefaults('addPost')
```

## `queryClient.setMutationDefaults`

`setMutationDefaults` can be used to set default options for specific mutations:

```js
queryClient.setMutationDefaults('addPost', { mutationFn: addPost })

function Component() {
  const { data } = useMutation('addPost')
}
```

**Options**

- `mutationKey: string | unknown[]`
- `options: MutationOptions`

## `queryClient.getQueryCache`

The `getQueryCache` method returns the query cache this client is connected to.

```js
const queryCache = queryClient.getQueryCache()
```

## `queryClient.getMutationCache`

The `getMutationCache` method returns the mutation cache this client is connected to.

```js
const mutationCache = queryClient.getMutationCache()
```

## `queryClient.clear`

The `clear` method clears all connected caches.

```js
queryClient.clear()
```


"test": "rescripts test",
"eject": "rescripts eject"
},
"dependencies": {
"@material-ui/core": "^4.9.7",
"axios": "0.19.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-query": "^3.5.0",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",


import { fireEvent, waitFor } from '@testing-library/react'
import React from 'react'

import { useMutation, QueryClient, QueryCache, MutationCache } from '../..'
import { UseMutationResult } from '../types'
import {
  mockConsoleError,
  mockNavigatorOnLine,
  queryKey,
  renderWithClient,
  setActTimeout,
  sleep,
} from './utils'

describe('useMutation', () => {
  const queryCache = new QueryCache()
  const mutationCache = new MutationCache()
  const queryClient = new QueryClient({ queryCache, mutationCache })

  it('should be able to reset `data`', async () => {
    function Page() {
      const { mutate, data = '', reset } = useMutation(() =>
        Promise.resolve('mutation')
      )

      return (

{data}

          <button onClick={() => reset()}>reset</button>
          <button onClick={() => mutate()}>mutate</button>

      )
    }

    const { getByTestId, getByText } = renderWithClient(queryClient, <Page />)

    expect(getByTestId('title').textContent).toBe('')

    fireEvent.click(getByText('mutate'))

    await waitFor(() => getByTestId('title'))

    expect(getByTestId('title').textContent).toBe('mutation')

    fireEvent.click(getByText('reset'))

    await waitFor(() => getByTestId('title'))

    expect(getByTestId('title').textContent).toBe('')
  })

  it('should be able to reset `error`', async () => {
    const consoleMock = mockConsoleError()

    function Page() {
      const { mutate, error, reset } = useMutation<string, Error>(() => {
        const err = new Error('Expected mock error. All is well!')
        err.stack = ''
        return Promise.reject(err)
      })

      return (

          {error &&
{error.message}
}
          <button onClick={() => reset()}>reset</button>
          <button onClick={() => mutate()}>mutate</button>

      )
    }

    const { getByTestId, getByText, queryByTestId } = renderWithClient(
      queryClient,
      <Page />
    )

    expect(queryByTestId('error')).toBeNull()

    fireEvent.click(getByText('mutate'))

    await waitFor(() => getByTestId('error'))

    expect(getByTestId('error').textContent).toBe(
      'Expected mock error. All is well!'
    )

    fireEvent.click(getByText('reset'))

    await waitFor(() => expect(queryByTestId('error')).toBeNull())

    consoleMock.mockRestore()
  })

  it('should be able to call `onSuccess` and `onSettled` after each successful mutate', async () => {
    let count = 0
    const onSuccessMock = jest.fn()
    const onSettledMock = jest.fn()

    function Page() {
      const { mutate } = useMutation(
        async (vars: { count: number }) => Promise.resolve(vars.count),
        {
          onSuccess: data => {
            onSuccessMock(data)
          },
          onSettled: data => {
            onSettledMock(data)
          },
        }
      )

      return (

{count}

          <button onClick={() => mutate({ count: ++count })}>mutate</button>

      )
    }

    const { getByTestId, getByText } = renderWithClient(queryClient, <Page />)

    expect(getByTestId('title').textContent).toBe('0')

    fireEvent.click(getByText('mutate'))
    fireEvent.click(getByText('mutate'))
    fireEvent.click(getByText('mutate'))

    await waitFor(() => getByTestId('title'))

    expect(onSuccessMock).toHaveBeenCalledTimes(3)
    expect(onSuccessMock).toHaveBeenCalledWith(1)
    expect(onSuccessMock).toHaveBeenCalledWith(2)
    expect(onSuccessMock).toHaveBeenCalledWith(3)

    expect(onSettledMock).toHaveBeenCalledTimes(3)
    expect(onSettledMock).toHaveBeenCalledWith(1)
    expect(onSettledMock).toHaveBeenCalledWith(2)
    expect(onSettledMock).toHaveBeenCalledWith(3)

    expect(getByTestId('title').textContent).toBe('3')
  })

  it('should be able to call `onError` and `onSettled` after each failed mutate', async () => {
    const consoleMock = mockConsoleError()

    const onErrorMock = jest.fn()
    const onSettledMock = jest.fn()
    let count = 0

    function Page() {
      const { mutate } = useMutation(
        (vars: { count: number }) => {
          const error = new Error(
            `Expected mock error. All is well! ${vars.count}`
          )
          error.stack = ''
          return Promise.reject(error)
        },
        {
          onError: (error: Error) => {
            onErrorMock(error.message)
          },
          onSettled: (_data, error) => {
            onSettledMock(error?.message)
          },
        }
      )

      return (

{count}

          <button onClick={() => mutate({ count: ++count })}>mutate</button>

      )
    }

    const { getByTestId, getByText } = renderWithClient(queryClient, <Page />)

    expect(getByTestId('title').textContent).toBe('0')

    fireEvent.click(getByText('mutate'))
    fireEvent.click(getByText('mutate'))
    fireEvent.click(getByText('mutate'))

    await waitFor(() => getByTestId('title'))

    expect(onErrorMock).toHaveBeenCalledTimes(3)
    expect(onErrorMock).toHaveBeenCalledWith(
      'Expected mock error. All is well! 1'
    )
    expect(onErrorMock).toHaveBeenCalledWith(
      'Expected mock error. All is well! 2'
    )
    expect(onErrorMock).toHaveBeenCalledWith(
      'Expected mock error. All is well! 3'
    )

    expect(onSettledMock).toHaveBeenCalledTimes(3)
    expect(onSettledMock).toHaveBeenCalledWith(
      'Expected mock error. All is well! 1'
    )
    expect(onSettledMock).toHaveBeenCalledWith(
      'Expected mock error. All is well! 2'
    )
    expect(onSettledMock).toHaveBeenCalledWith(
      'Expected mock error. All is well! 3'
    )

    expect(getByTestId('title').textContent).toBe('3')

    consoleMock.mockRestore()
  })

  it('should be able to override the useMutation success callbacks', async () => {
    const callbacks: string[] = []

    function Page() {
      const { mutateAsync } = useMutation(async (text: string) => text, {
        onSuccess: async () => {
          callbacks.push('useMutation.onSuccess')
        },
        onSettled: async () => {
          callbacks.push('useMutation.onSettled')
        },
      })

      React.useEffect(() => {
        setActTimeout(async () => {
          try {
            const result = await mutateAsync('todo', {
              onSuccess: async () => {
                callbacks.push('mutateAsync.onSuccess')
              },
              onSettled: async () => {
                callbacks.push('mutateAsync.onSettled')
              },
            })
            callbacks.push(`mutateAsync.result:${result}`)
          } catch {}
        }, 10)
      }, [mutateAsync])

      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(100)

    expect(callbacks).toEqual([
      'useMutation.onSuccess',
      'useMutation.onSettled',
      'mutateAsync.onSuccess',
      'mutateAsync.onSettled',
      'mutateAsync.result:todo',
    ])
  })

  it('should be able to override the error callbacks when using mutateAsync', async () => {
    const consoleMock = mockConsoleError()

    const callbacks: string[] = []

    function Page() {
      const { mutateAsync } = useMutation(
        async (_text: string) => Promise.reject('oops'),
        {
          onError: async () => {
            callbacks.push('useMutation.onError')
          },
          onSettled: async () => {
            callbacks.push('useMutation.onSettled')
          },
        }
      )

      React.useEffect(() => {
        setActTimeout(async () => {
          try {
            await mutateAsync('todo', {
              onError: async () => {
                callbacks.push('mutateAsync.onError')
              },
              onSettled: async () => {
                callbacks.push('mutateAsync.onSettled')
              },
            })
          } catch (error) {
            callbacks.push(`mutateAsync.error:${error}`)
          }
        }, 10)
      }, [mutateAsync])

      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(100)

    expect(callbacks).toEqual([
      'useMutation.onError',
      'useMutation.onSettled',
      'mutateAsync.onError',
      'mutateAsync.onSettled',
      'mutateAsync.error:oops',
    ])

    consoleMock.mockRestore()
  })

  it('should be able to use mutation defaults', async () => {
    const key = queryKey()

    queryClient.setMutationDefaults(key, {
      mutationFn: async (text: string) => text,
    })

    const states: UseMutationResult<any, any, any, any>[] = []

    function Page() {
      const state = useMutation<string, unknown, string>(key)

      states.push(state)

      const { mutate } = state

      React.useEffect(() => {
        setActTimeout(() => {
          mutate('todo')
        }, 10)
      }, [mutate])

      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(100)

    expect(states.length).toBe(3)
    expect(states[0]).toMatchObject({ data: undefined, isLoading: false })
    expect(states[1]).toMatchObject({ data: undefined, isLoading: true })
    expect(states[2]).toMatchObject({ data: 'todo', isLoading: false })
  })

  it('should be able to retry a failed mutation', async () => {
    const consoleMock = mockConsoleError()

    let count = 0

    function Page() {
      const { mutate } = useMutation(
        (_text: string) => {
          count++
          return Promise.reject('oops')
        },
        {
          retry: 1,
          retryDelay: 5,
        }
      )

      React.useEffect(() => {
        setActTimeout(() => {
          mutate('todo')
        }, 10)
      }, [mutate])

      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(100)

    expect(count).toBe(2)

    consoleMock.mockRestore()
  })

  it('should be able to retry a mutation when online', async () => {
    const consoleMock = mockConsoleError()
    mockNavigatorOnLine(false)

    let count = 0
    const states: UseMutationResult<any, any, any, any>[] = []

    function Page() {
      const state = useMutation(
        (_text: string) => {
          count++
          return count > 1 ? Promise.resolve('data') : Promise.reject('oops')
        },
        {
          retry: 1,
          retryDelay: 5,
        }
      )

      states.push(state)

      const { mutate } = state

      React.useEffect(() => {
        setActTimeout(() => {
          mutate('todo')
        }, 10)
      }, [mutate])

      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(50)

    expect(states.length).toBe(4)
    expect(states[0]).toMatchObject({
      isLoading: false,
      isPaused: false,
      failureCount: 0,
    })
    expect(states[1]).toMatchObject({
      isLoading: true,
      isPaused: false,
      failureCount: 0,
    })
    expect(states[2]).toMatchObject({
      isLoading: true,
      isPaused: false,
      failureCount: 1,
    })
    expect(states[3]).toMatchObject({
      isLoading: true,
      isPaused: true,
      failureCount: 1,
    })

    mockNavigatorOnLine(true)
    window.dispatchEvent(new Event('online'))

    await sleep(50)

    expect(states.length).toBe(6)
    expect(states[4]).toMatchObject({
      isLoading: true,
      isPaused: false,
      failureCount: 1,
    })
    expect(states[5]).toMatchObject({
      isLoading: false,
      isPaused: false,
      failureCount: 1,
      data: 'data',
    })

    consoleMock.mockRestore()
  })

  it('should not change state if unmounted', async () => {
    function Mutates() {
      const { mutate } = useMutation(() => sleep(10))
      return <button onClick={() => mutate()}>mutate</button>
    }
    function Page() {
      const [mounted, setMounted] = React.useState(true)
      return (

          <button onClick={() => setMounted(false)}>unmount</button>
          {mounted && <Mutates />}

      )
    }

    const { getByText } = renderWithClient(queryClient, <Page />)
    fireEvent.click(getByText('mutate'))
    fireEvent.click(getByText('unmount'))
  })
})


import { scheduleMicrotask } from './utils'

// TYPES

type NotifyCallback = () => void

type NotifyFunction = (callback: () => void) => void

type BatchNotifyFunction = (callback: () => void) => void

// CLASS

class NotifyManager {
  private queue: NotifyCallback[]
  private transactions: number
  private notifyFn: NotifyFunction
  private batchNotifyFn: BatchNotifyFunction

  constructor() {
    this.queue = []
    this.transactions = 0

    this.notifyFn = (callback: () => void) => {
      callback()
    }

    this.batchNotifyFn = (callback: () => void) => {
      callback()
    }
  }

  batch<T>(callback: () => T): T {
    this.transactions++
    const result = callback()
    this.transactions--
    if (!this.transactions) {
      this.flush()
    }
    return result
  }

  schedule(callback: NotifyCallback): void {
    if (this.transactions) {
      this.queue.push(callback)
    } else {
      scheduleMicrotask(() => {
        this.notifyFn(callback)
      })
    }
  }

  /**
   * All calls to the wrapped function will be batched.
   */
  batchCalls<T extends Function>(callback: T): T {
    return ((...args: any[]) => {
      this.schedule(() => {
        callback(...args)
      })
    }) as any
  }

  flush(): void {
    const queue = this.queue
    this.queue = []
    if (queue.length) {
      scheduleMicrotask(() => {
        this.batchNotifyFn(() => {
          queue.forEach(callback => {
            this.notifyFn(callback)
          })
        })
      })
    }
  }

  /**
   * Use this method to set a custom notify function.
   * This can be used to for example wrap notifications with `React.act` while running tests.
   */
  setNotifyFunction(fn: NotifyFunction) {
    this.notifyFn = fn
  }

  /**
   * Use this method to set a custom function to batch notifications together into a single tick.
   * By default React Query will use the batch function provided by ReactDOM or React Native.
   */
  setBatchNotifyFunction(fn: BatchNotifyFunction) {
    this.batchNotifyFn = fn
  }
}

// SINGLETON

export const notifyManager = new NotifyManager()


---
id: QueriesObserver
title: QueriesObserver
---

## `QueriesObserver`

The `QueriesObserver` can be used to observe multiple queries.

```js
const observer = new QueriesObserver(queryClient, [
  { queryKey: ['post', 1], queryFn: fetchPost },
  { queryKey: ['post', 2], queryFn: fetchPost },
])

const unsubscribe = observer.subscribe(result => {
  console.log(result)
  unsubscribe()
})
```

**Options**

The options for the `QueriesObserver` are exactly the same as those of [`useQueries`](#usequeries).


keep keep keep replace keep keep keep keep keep


import React from 'react'

import { notifyManager } from '../core/notifyManager'
import { QueryObserver } from '../core/queryObserver'
import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary'
import { useQueryClient } from './QueryClientProvider'
import { UseBaseQueryOptions } from './types'

export function useBaseQuery<TQueryFnData, TError, TData, TQueryData>(
  options: UseBaseQueryOptions<TQueryFnData, TError, TData, TQueryData>,
  Observer: typeof QueryObserver
) {
  const queryClient = useQueryClient()
  const errorResetBoundary = useQueryErrorResetBoundary()
  const defaultedOptions = queryClient.defaultQueryObserverOptions(options)

  // Include callbacks in batch renders
  if (defaultedOptions.onError) {
    defaultedOptions.onError = notifyManager.batchCalls(
      defaultedOptions.onError
    )
  }

  if (defaultedOptions.onSuccess) {
    defaultedOptions.onSuccess = notifyManager.batchCalls(
      defaultedOptions.onSuccess
    )
  }

  if (defaultedOptions.onSettled) {
    defaultedOptions.onSettled = notifyManager.batchCalls(
      defaultedOptions.onSettled
    )
  }

  if (defaultedOptions.suspense) {
    // Always set stale time when using suspense to prevent
    // fetching again when directly re-mounting after suspense
    if (typeof defaultedOptions.staleTime !== 'number') {
      defaultedOptions.staleTime = 1000
    }

    // Prevent retrying failed query if the error boundary has not been reset yet
    if (!errorResetBoundary.isReset()) {
      defaultedOptions.retryOnMount = false
    }
  }

  // Create query observer
  const observerRef = React.useRef<QueryObserver<any, any, any, any>>()
  const observer =
    observerRef.current || new Observer(queryClient, defaultedOptions)
  observerRef.current = observer

  // Update options
  if (observer.hasListeners()) {
    observer.setOptions(defaultedOptions)
  }

  const currentResult = observer.getCurrentResult()
  const [, setCurrentResult] = React.useState(currentResult)

  // Subscribe to the observer
  React.useEffect(() => {
    errorResetBoundary.clearReset()
    return observer.subscribe(notifyManager.batchCalls(setCurrentResult))
  }, [observer, errorResetBoundary])

  // Handle suspense
  if (observer.options.suspense || observer.options.useErrorBoundary) {
    if (observer.options.suspense && currentResult.isLoading) {
      errorResetBoundary.clearReset()
      const unsubscribe = observer.subscribe()
      throw observer.refetch().finally(unsubscribe)
    }

    if (currentResult.isError) {
      throw currentResult.error
    }
  }

  return observer.options.notifyOnChangeProps === 'tracked'
    ? observer.getTrackedCurrentResult()
    : currentResult
}


import React, { Component } from 'react'
import IObserver from './intersection-observer'
import cn from 'classnames'

// import VideoComponent from '../styleguide/video';

// This component might look a little complex
// because one could argue that keeping the aspect ratio
// of an image can be solved with `height: auto`,
// but it's actually not that easy if you want to prevent
// element flickering

// Because if you want to do that, you need to set the aspect
// ratio of the image's container BEFORE the image loads

class Image extends Component {
  constructor(props) {
    super(props)
  }

  static defaultProps = {
    lazy: true,
  }

  state = {
    src: !this.props.lazy ? this.props.videoSrc || this.props.src : undefined,
  }

  handleIntersect = entry => {
    if (entry.isIntersecting) {
      this.setState({ src: this.props.src })
    }
  }

  onLoad = () => {
    this.setState({
      loaded: true,
    })
  }

  render() {
    const {
      caption,
      height: origHeight,
      lazy,
      margin = 40,
      video = false,
      videoSrc,
      width: origWidth,
      captionSpacing = null,
      oversize = true,
      borderRadius = false,
      alt,
      imageProps,
      className,
      children,
      ...props
    } = this.props
    const height = parseInt(origHeight)
    const width = parseInt(origWidth)

    const aspectRatio = String((height / width) * 100) + '%'

    // if (video || videoSrc) {
    //   return <VideoComponent src={videoSrc} {...this.props} />;
    // }

    let src = this.state.src

    // if (src && src.indexOf('assets.zeit.co') > -1) {
    //   if (src.indexOf('/q_auto') === -1) {
    //     src = src.replace('upload/', 'upload/q_auto/');
    //   }
    // }

    return (
      <IObserver
        once
        onIntersect={this.handleIntersect}
        rootMargin="20%"
        disabled={!lazy}
      >

 650 && oversize,
            fadeIn: this.state.loaded,
          })}
          {...props}
        >

              {src ? (

              ) : (
                children
              )}

            {caption && (

                {caption}

            )}

      </IObserver>
    )
  }
}

// export const Video = (props) => <Image {...props} video />;

export default Image


import {
  getStatusProps,
  isServer,
  isValidTimeout,
  noop,
  replaceEqualDeep,
  shallowEqualObjects,
  timeUntilStale,
} from './utils'
import { notifyManager } from './notifyManager'
import type {
  PlaceholderDataFunction,
  QueryObserverBaseResult,
  QueryObserverOptions,
  QueryObserverResult,
  QueryOptions,
  RefetchOptions,
  ResultOptions,
} from './types'
import type { Query, QueryState, Action, FetchOptions } from './query'
import type { QueryClient } from './queryClient'
import { focusManager } from './focusManager'
import { Subscribable } from './subscribable'

type QueryObserverListener<TData, TError> = (
  result: QueryObserverResult<TData, TError>
) => void

interface NotifyOptions {
  cache?: boolean
  listeners?: boolean
  onError?: boolean
  onSuccess?: boolean
}

export interface ObserverFetchOptions extends FetchOptions {
  throwOnError?: boolean
}

export class QueryObserver<
  TQueryFnData = unknown,
  TError = unknown,
  TData = TQueryFnData,
  TQueryData = TQueryFnData
> extends Subscribable<QueryObserverListener<TData, TError>> {
  options: QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>

  private client: QueryClient
  private currentQuery!: Query<TQueryFnData, TError, TQueryData>
  private currentResult!: QueryObserverResult<TData, TError>
  private currentResultState?: QueryState<TQueryData, TError>
  private previousQueryResult?: QueryObserverResult<TData, TError>
  private initialDataUpdateCount: number
  private initialErrorUpdateCount: number
  private staleTimeoutId?: number
  private refetchIntervalId?: number
  private trackedProps!: Array<keyof QueryObserverResult>
  private trackedCurrentResult!: QueryObserverResult<TData, TError>

  constructor(
    client: QueryClient,
    options: QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>
  ) {
    super()

    this.client = client
    this.options = options
    this.initialDataUpdateCount = 0
    this.initialErrorUpdateCount = 0
    this.trackedProps = []
    this.bindMethods()
    this.setOptions(options)
  }

  protected bindMethods(): void {
    this.remove = this.remove.bind(this)
    this.refetch = this.refetch.bind(this)
  }

  protected onSubscribe(): void {
    if (this.listeners.length === 1) {
      this.updateQuery()

      this.currentQuery.addObserver(this)

      if (this.willFetchOnMount()) {
        this.executeFetch()
      }

      this.updateTimers()
    }
  }

  protected onUnsubscribe(): void {
    if (!this.listeners.length) {
      this.destroy()
    }
  }

  willLoadOnMount(): boolean {
    return (
      this.options.enabled !== false &&
      !this.currentQuery.state.dataUpdatedAt &&
      !(
        this.currentQuery.state.status === 'error' &&
        this.options.retryOnMount === false
      )
    )
  }

  willRefetchOnMount(): boolean {
    return (
      this.options.enabled !== false &&
      this.currentQuery.state.dataUpdatedAt > 0 &&
      (this.options.refetchOnMount === 'always' ||
        (this.options.refetchOnMount !== false && this.isStale()))
    )
  }

  willFetchOnMount(): boolean {
    return this.willLoadOnMount() || this.willRefetchOnMount()
  }

  willFetchOnReconnect(): boolean {
    return (
      this.options.enabled !== false &&
      (this.options.refetchOnReconnect === 'always' ||
        (this.options.refetchOnReconnect !== false && this.isStale()))
    )
  }

  willFetchOnWindowFocus(): boolean {
    return (
      this.options.enabled !== false &&
      (this.options.refetchOnWindowFocus === 'always' ||
        (this.options.refetchOnWindowFocus !== false && this.isStale()))
    )
  }

  private willFetchOptionally(): boolean {
    return this.options.enabled !== false && this.isStale()
  }

  private isStale(): boolean {
    return this.currentQuery.isStaleByTime(this.options.staleTime)
  }

  destroy(): void {
    this.listeners = []
    this.clearTimers()
    this.currentQuery.removeObserver(this)
  }

  setOptions(
    options?: QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>
  ): void {
    const prevOptions = this.options
    const prevQuery = this.currentQuery

    this.options = this.client.defaultQueryObserverOptions(options)

    if (
      typeof this.options.enabled !== 'undefined' &&
      typeof this.options.enabled !== 'boolean'
    ) {
      throw new Error('Expected enabled to be a boolean')
    }

    // Keep previous query key if the user does not supply one
    if (!this.options.queryKey) {
      this.options.queryKey = prevOptions.queryKey
    }

    this.updateQuery()

    // Take no further actions if there are no subscribers
    if (!this.listeners.length) {
      return
    }

    // If we subscribed to a new query, optionally fetch and update refetch
    if (this.currentQuery !== prevQuery) {
      this.optionalFetch()
      this.updateTimers()
      return
    }

    // Optionally fetch if the query became enabled
    if (this.options.enabled !== false && prevOptions.enabled === false) {
      this.optionalFetch()
    }

    // Update stale interval if needed
    if (
      this.options.enabled !== prevOptions.enabled ||
      this.options.staleTime !== prevOptions.staleTime
    ) {
      this.updateStaleTimeout()
    }

    // Update refetch interval if needed
    if (
      this.options.enabled !== prevOptions.enabled ||
      this.options.refetchInterval !== prevOptions.refetchInterval
    ) {
      this.updateRefetchInterval()
    }
  }

  getCurrentResult(): QueryObserverResult<TData, TError> {
    return this.currentResult
  }

  getTrackedCurrentResult(): QueryObserverResult<TData, TError> {
    return this.trackedCurrentResult
  }

  getNextResult(
    options?: ResultOptions
  ): Promise<QueryObserverResult<TData, TError>> {
    return new Promise((resolve, reject) => {
      const unsubscribe = this.subscribe(result => {
        if (!result.isFetching) {
          unsubscribe()
          if (result.isError && options?.throwOnError) {
            reject(result.error)
          } else {
            resolve(result)
          }
        }
      })
    })
  }

  getCurrentQuery(): Query<TQueryFnData, TError, TQueryData> {
    return this.currentQuery
  }

  remove(): void {
    this.client.getQueryCache().remove(this.currentQuery)
  }

  refetch(
    options?: RefetchOptions
  ): Promise<QueryObserverResult<TData, TError>> {
    return this.fetch(options)
  }

  protected fetch(
    fetchOptions?: ObserverFetchOptions
  ): Promise<QueryObserverResult<TData, TError>> {
    return this.executeFetch(fetchOptions).then(() => {
      this.updateResult()
      return this.currentResult
    })
  }

  private optionalFetch(): void {
    if (this.willFetchOptionally()) {
      this.executeFetch()
    }
  }

  private executeFetch(
    fetchOptions?: ObserverFetchOptions
  ): Promise<TQueryData | undefined> {
    // Make sure we reference the latest query as the current one might have been removed
    this.updateQuery()

    // Fetch
    let promise: Promise<TQueryData | undefined> = this.currentQuery.fetch(
      this.options as QueryOptions<TQueryFnData, TError, TQueryData>,
      fetchOptions
    )

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

    return promise
  }

  private updateStaleTimeout(): void {
    this.clearStaleTimeout()

    if (
      isServer ||
      this.currentResult.isStale ||
      !isValidTimeout(this.options.staleTime)
    ) {
      return
    }

    const time = timeUntilStale(
      this.currentResult.dataUpdatedAt,
      this.options.staleTime
    )

    // The timeout is sometimes triggered 1 ms before the stale time expiration.
    // To mitigate this issue we always add 1 ms to the timeout.
    const timeout = time + 1

    this.staleTimeoutId = setTimeout(() => {
      if (!this.currentResult.isStale) {
        const prevResult = this.currentResult
        this.updateResult()
        this.notify({
          listeners: this.shouldNotifyListeners(prevResult, this.currentResult),
          cache: true,
        })
      }
    }, timeout)
  }

  private updateRefetchInterval(): void {
    this.clearRefetchInterval()

    if (
      isServer ||
      this.options.enabled === false ||
      !isValidTimeout(this.options.refetchInterval)
    ) {
      return
    }

    this.refetchIntervalId = setInterval(() => {
      if (
        this.options.refetchIntervalInBackground ||
        focusManager.isFocused()
      ) {
        this.executeFetch()
      }
    }, this.options.refetchInterval)
  }

  private updateTimers(): void {
    this.updateStaleTimeout()
    this.updateRefetchInterval()
  }

  private clearTimers(): void {
    this.clearStaleTimeout()
    this.clearRefetchInterval()
  }

  private clearStaleTimeout(): void {
    clearTimeout(this.staleTimeoutId)
    this.staleTimeoutId = undefined
  }

  private clearRefetchInterval(): void {
    clearInterval(this.refetchIntervalId)
    this.refetchIntervalId = undefined
  }

  protected getNewResult(
    willFetch?: boolean
  ): QueryObserverResult<TData, TError> {
    const { state } = this.currentQuery
    let { isFetching, status } = state
    let isPreviousData = false
    let isPlaceholderData = false
    let data: TData | undefined
    let dataUpdatedAt = state.dataUpdatedAt

    // Optimistically set status to loading if we will start fetching
    if (willFetch) {
      isFetching = true
      if (!dataUpdatedAt) {
        status = 'loading'
      }
    }

    // Keep previous data if needed
    if (
      this.options.keepPreviousData &&
      !state.dataUpdateCount &&
      this.previousQueryResult?.isSuccess &&
      status !== 'error'
    ) {
      data = this.previousQueryResult.data
      dataUpdatedAt = this.previousQueryResult.dataUpdatedAt
      status = this.previousQueryResult.status
      isPreviousData = true
    }
    // Select data if needed
    else if (this.options.select && typeof state.data !== 'undefined') {
      // Use the previous select result if the query data did not change
      if (this.currentResult && state.data === this.currentResultState?.data) {
        data = this.currentResult.data
      } else {
        data = this.options.select(state.data)
        if (this.options.structuralSharing !== false) {
          data = replaceEqualDeep(this.currentResult?.data, data)
        }
      }
    }
    // Use query data
    else {
      data = (state.data as unknown) as TData
    }

    // Show placeholder data if needed
    if (
      typeof this.options.placeholderData !== 'undefined' &&
      typeof data === 'undefined' &&
      status === 'loading'
    ) {
      const placeholderData =
        typeof this.options.placeholderData === 'function'
          ? (this.options.placeholderData as PlaceholderDataFunction<TData>)()
          : this.options.placeholderData
      if (typeof placeholderData !== 'undefined') {
        status = 'success'
        data = placeholderData
        isPlaceholderData = true
      }
    }

    const result: QueryObserverBaseResult<TData, TError> = {
      ...getStatusProps(status),
      data,
      dataUpdatedAt,
      error: state.error,
      errorUpdatedAt: state.errorUpdatedAt,
      failureCount: state.fetchFailureCount,
      isFetched: state.dataUpdateCount > 0 || state.errorUpdateCount > 0,
      isFetchedAfterMount:
        state.dataUpdateCount > this.initialDataUpdateCount ||
        state.errorUpdateCount > this.initialErrorUpdateCount,
      isFetching,
      isLoadingError: status === 'error' && state.dataUpdatedAt === 0,
      isPlaceholderData,
      isPreviousData,
      isRefetchError: status === 'error' && state.dataUpdatedAt !== 0,
      isStale: this.isStale(),
      refetch: this.refetch,
      remove: this.remove,
    }

    return result as QueryObserverResult<TData, TError>
  }

  private shouldNotifyListeners(
    prevResult: QueryObserverResult,
    result: QueryObserverResult
  ): boolean {
    const { notifyOnChangeProps, notifyOnChangePropsExclusions } = this.options

    if (prevResult === result) {
      return false
    }

    if (!notifyOnChangeProps && !notifyOnChangePropsExclusions) {
      return true
    }

    const keys = Object.keys(result)
    const includedProps =
      notifyOnChangeProps === 'tracked'
        ? this.trackedProps
        : notifyOnChangeProps

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i] as keyof QueryObserverResult
      const changed = prevResult[key] !== result[key]
      const isIncluded = includedProps?.some(x => x === key)
      const isExcluded = notifyOnChangePropsExclusions?.some(x => x === key)

      if (changed) {
        if (notifyOnChangePropsExclusions && isExcluded) {
          continue
        }

        if (
          !notifyOnChangeProps ||
          isIncluded ||
          (notifyOnChangeProps === 'tracked' && this.trackedProps.length === 0)
        ) {
          return true
        }
      }
    }

    return false
  }

  private updateResult(willFetch?: boolean): void {
    const result = this.getNewResult(willFetch)

    // Keep reference to the current state on which the current result is based on
    this.currentResultState = this.currentQuery.state

    // Only update if something has changed
    if (!shallowEqualObjects(result, this.currentResult)) {
      this.currentResult = result

      if (this.options.notifyOnChangeProps === 'tracked') {
        const addTrackedProps = (prop: keyof QueryObserverResult) => {
          if (!this.trackedProps.includes(prop)) {
            this.trackedProps.push(prop)
          }
        }
        this.trackedCurrentResult = {} as QueryObserverResult<TData, TError>

        Object.keys(result).forEach(key => {
          Object.defineProperty(this.trackedCurrentResult, key, {
            configurable: false,
            enumerable: true,
            get() {
              addTrackedProps(key as keyof QueryObserverResult)
              return result[key as keyof QueryObserverResult]
            },
          })
        })
      }
    }
  }

  private updateQuery(): void {
    const prevQuery = this.currentQuery

    const query = this.client
      .getQueryCache()
      .build(
        this.client,
        this.options as QueryOptions<TQueryFnData, TError, TQueryData>
      )

    if (query === prevQuery) {
      return
    }

    this.previousQueryResult = this.currentResult
    this.currentQuery = query
    this.initialDataUpdateCount = query.state.dataUpdateCount
    this.initialErrorUpdateCount = query.state.errorUpdateCount

    const willFetch = prevQuery
      ? this.willFetchOptionally()
      : this.willFetchOnMount()

    this.updateResult(willFetch)

    if (!this.hasListeners()) {
      return
    }

    prevQuery?.removeObserver(this)
    this.currentQuery.addObserver(this)

    if (
      this.shouldNotifyListeners(this.previousQueryResult, this.currentResult)
    ) {
      this.notify({ listeners: true })
    }
  }

  onQueryUpdate(action: Action<TData, TError>): void {
    // Store current result and get new result
    const prevResult = this.currentResult
    this.updateResult()
    const currentResult = this.currentResult

    // Update timers
    this.updateTimers()

    // Do not notify if the nothing has changed
    if (prevResult === currentResult) {
      return
    }

    // Determine which callbacks to trigger
    const notifyOptions: NotifyOptions = {}

    if (action.type === 'success') {
      notifyOptions.onSuccess = true
    } else if (action.type === 'error') {
      notifyOptions.onError = true
    }

    if (this.shouldNotifyListeners(prevResult, currentResult)) {
      notifyOptions.listeners = true
    }

    this.notify(notifyOptions)
  }

  private notify(notifyOptions: NotifyOptions): void {
    notifyManager.batch(() => {
      // First trigger the configuration callbacks
      if (notifyOptions.onSuccess) {
        this.options.onSuccess?.(this.currentResult.data!)
        this.options.onSettled?.(this.currentResult.data!, null)
      } else if (notifyOptions.onError) {
        this.options.onError?.(this.currentResult.error!)
        this.options.onSettled?.(undefined, this.currentResult.error!)
      }

      // Then trigger the listeners
      if (notifyOptions.listeners) {
        this.listeners.forEach(listener => {
          listener(this.currentResult)
        })
      }

      // Then the cache listeners
      if (notifyOptions.cache) {
        this.client.getQueryCache().notify(this.currentQuery)
      }
    })
  }
}