diff --git a/.github/workflows/test-and-publish.yml b/.github/workflows/test-and-publish.yml
index 8c2e52ac94..24d468633e 100644
--- a/.github/workflows/test-and-publish.yml
+++ b/.github/workflows/test-and-publish.yml
@@ -5,6 +5,7 @@ on:
branches:
- 'master'
- 'next'
+ - 'alpha'
- 'beta'
- '1.x'
- '2.x'
@@ -36,7 +37,7 @@ jobs:
name: 'Publish Module to NPM'
needs: test
# publish only when merged in master on original repo, not on PR
- if: github.repository == 'tannerlinsley/react-query' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/next' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/1.x' || github.ref == 'refs/heads/2.x')
+ if: github.repository == 'tannerlinsley/react-query' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/next' || github.ref == 'refs/heads/alpha' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/1.x' || github.ref == 'refs/heads/2.x')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
diff --git a/createAsyncStoragePersister/package.json b/createAsyncStoragePersister/package.json
new file mode 100644
index 0000000000..4d74825ea3
--- /dev/null
+++ b/createAsyncStoragePersister/package.json
@@ -0,0 +1,6 @@
+{
+ "internal": true,
+ "main": "../lib/createAsyncStoragePersister/index.js",
+ "module": "../es/createAsyncStoragePersister/index.js",
+ "types": "../types/createAsyncStoragePersister/index.d.ts"
+}
diff --git a/createAsyncStoragePersistor-experimental/package.json b/createAsyncStoragePersistor-experimental/package.json
deleted file mode 100644
index 6a1bf0b2cb..0000000000
--- a/createAsyncStoragePersistor-experimental/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "internal": true,
- "main": "../lib/createAsyncStoragePersistor-experimental/index.js",
- "module": "../es/createAsyncStoragePersistor-experimental/index.js",
- "types": "../types/createAsyncStoragePersistor-experimental/index.d.ts"
-}
diff --git a/createWebStoragePersister/package.json b/createWebStoragePersister/package.json
new file mode 100644
index 0000000000..21c28c7658
--- /dev/null
+++ b/createWebStoragePersister/package.json
@@ -0,0 +1,6 @@
+{
+ "internal": true,
+ "main": "../lib/createWebStoragePersister/index.js",
+ "module": "../es/createWebStoragePersister/index.js",
+ "types": "../types/createWebStoragePersister/index.d.ts"
+}
diff --git a/createWebStoragePersistor-experimental/package.json b/createWebStoragePersistor-experimental/package.json
deleted file mode 100644
index 2f4a1486c3..0000000000
--- a/createWebStoragePersistor-experimental/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "internal": true,
- "main": "../lib/createWebStoragePersistor-experimental/index.js",
- "module": "../es/createWebStoragePersistor-experimental/index.js",
- "types": "../types/createWebStoragePersistor-experimental/index.d.ts"
-}
diff --git a/docs/src/manifests/manifest.json b/docs/src/manifests/manifest.json
index 18ad4cd4e5..fd3cdf8274 100644
--- a/docs/src/manifests/manifest.json
+++ b/docs/src/manifests/manifest.json
@@ -75,6 +75,11 @@
"path": "/guides/query-functions",
"editUrl": "/guides/query-functions.md"
},
+ {
+ "title": "Network Mode",
+ "path": "/guides/network-mode",
+ "editUrl": "/guides/network-mode.md"
+ },
{
"title": "Parallel Queries",
"path": "/guides/parallel-queries",
@@ -204,6 +209,11 @@
"title": "Migrating to React Query 3",
"path": "/guides/migrating-to-react-query-3",
"editUrl": "/guides/migrating-to-react-query-3.md"
+ },
+ {
+ "title": "Migrating to React Query 4",
+ "path": "/guides/migrating-to-react-query-4",
+ "editUrl": "/guides/migrating-to-react-query-4.md"
}
]
},
@@ -314,19 +324,19 @@
"heading": true,
"routes": [
{
- "title": "persistQueryClient (Experimental)",
+ "title": "persistQueryClient",
"path": "/plugins/persistQueryClient",
"editUrl": "/plugins/persistQueryClient.md"
},
{
- "title": "createWebStoragePersistor (Experimental)",
- "path": "/plugins/createWebStoragePersistor",
- "editUrl": "/plugins/createWebStoragePersistor.md"
+ "title": "createWebStoragePersister",
+ "path": "/plugins/createWebStoragePersister",
+ "editUrl": "/plugins/createWebStoragePersister.md"
},
{
- "title": "createAsyncStoragePersistor (Experimental)",
- "path": "/plugins/createAsyncStoragePersistor",
- "editUrl": "/plugins/createAsyncStoragePersistor.md"
+ "title": "createAsyncStoragePersister",
+ "path": "/plugins/createAsyncStoragePersister",
+ "editUrl": "/plugins/createAsyncStoragePersister.md"
},
{
"title": "broadcastQueryClient (Experimental)",
@@ -442,4 +452,4 @@
]
}
]
-}
\ No newline at end of file
+}
diff --git a/docs/src/pages/comparison.md b/docs/src/pages/comparison.md
index 4a3691ed4c..dfa06c741d 100644
--- a/docs/src/pages/comparison.md
+++ b/docs/src/pages/comparison.md
@@ -64,7 +64,7 @@ Feature/Capability Key:
> **1 Lagged Query Data** - React Query provides a way to continue to see an existing query's data while the next query loads (similar to the same UX that suspense will soon provide natively). This is extremely important when writing pagination UIs or infinite loading UIs where you do not want to show a hard loading state whenever a new query is requested. Other libraries do not have this capability and render a hard loading state for the new query (unless it has been prefetched), while the new query loads.
-> **2 Render Optimization** - React Query has excellent rendering performance. It will only re-render your components when a query is updated. For example because it has new data, or to indicate it is fetching. React Query also batches updates together to make sure your application only re-renders once when multiple components are using the same query. If you are only interested in the `data` or `error` properties, you can reduce the number of renders even more by setting `notifyOnChangeProps` to `['data', 'error']`. Set `notifyOnChangeProps: 'tracked'` to automatically track which fields are accessed and only re-render if one of them changes.
+> **2 Render Optimization** - React Query has excellent rendering performance. By default, it will automatically track which fields are accessed and only re-render if one of them changes. If you would like to opt-out of this optimization, setting `notifyOnChangeProps` to `'all'` will re-render your components whenever the query is updated. For example because it has new data, or to indicate it is fetching. React Query also batches updates together to make sure your application only re-renders once when multiple components are using the same query. If you are only interested in the `data` or `error` properties, you can reduce the number of renders even more by setting `notifyOnChangeProps` to `['data', 'error']`.
> **3 Partial query matching** - Because React Query uses deterministic query key serialization, this allows you to manipulate variable groups of queries without having to know each individual query-key that you want to match, eg. you can refetch every query that starts with `todos` in its key, regardless of variables, or you can target specific queries with (or without) variables or nested properties, and even use a filter function to only match queries that pass your specific conditions.
diff --git a/docs/src/pages/guides/caching.md b/docs/src/pages/guides/caching.md
index 2f8b605328..e200b53721 100644
--- a/docs/src/pages/guides/caching.md
+++ b/docs/src/pages/guides/caching.md
@@ -16,17 +16,17 @@ This caching example illustrates the story and lifecycle of:
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.
+- 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.
+ - It will then cache the data using `['todos']` 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.
+- A second instance of `useQuery(['todos'], fetchTodos)` mounts elsewhere.
- Because this exact data exists 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.
+- Both instances of the `useQuery(['todos'], fetchTodos)` query are unmounted and no longer in use.
- Since there are no more active instances of 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**.
+- 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.
diff --git a/docs/src/pages/guides/default-query-function.md b/docs/src/pages/guides/default-query-function.md
index 0ec3364c1f..7561ae375d 100644
--- a/docs/src/pages/guides/default-query-function.md
+++ b/docs/src/pages/guides/default-query-function.md
@@ -7,7 +7,6 @@ If you find yourself wishing for whatever reason that you could just share the s
```js
// Define a default query function that will receive the query key
-// the queryKey is guaranteed to be an Array here
const defaultQueryFn = async ({ queryKey }) => {
const { data } = await axios.get(`https://jsonplaceholder.typicode.com${queryKey[0]}`);
return data;
@@ -32,14 +31,14 @@ function App() {
// All you have to do now is pass a key!
function Posts() {
- const { status, data, error, isFetching } = useQuery('/posts')
+ const { status, data, error, isFetching } = useQuery(['/posts'])
// ...
}
// You can even leave out the queryFn and just go straight into options
function Post({ postId }) {
- const { status, data, error, isFetching } = useQuery(`/posts/${postId}`, {
+ const { status, data, error, isFetching } = useQuery([`/posts/${postId}`], {
enabled: !!postId,
})
diff --git a/docs/src/pages/guides/disabling-queries.md b/docs/src/pages/guides/disabling-queries.md
index 8162e850b8..ec28592be2 100644
--- a/docs/src/pages/guides/disabling-queries.md
+++ b/docs/src/pages/guides/disabling-queries.md
@@ -26,7 +26,7 @@ function Todos() {
error,
refetch,
isFetching,
- } = useQuery('todos', fetchTodoList, {
+ } = useQuery(['todos'], fetchTodoList, {
enabled: false,
})
diff --git a/docs/src/pages/guides/filters.md b/docs/src/pages/guides/filters.md
index 9419e44d11..79e6f1cda6 100644
--- a/docs/src/pages/guides/filters.md
+++ b/docs/src/pages/guides/filters.md
@@ -14,31 +14,30 @@ A query filter is an object with certain conditions to match a query with:
await queryClient.cancelQueries()
// Remove all inactive queries that begin with `posts` in the key
-queryClient.removeQueries('posts', { inactive: true })
+queryClient.removeQueries(['posts'], { type: 'inactive' })
// Refetch all active queries
-await queryClient.refetchQueries({ active: true })
+await queryClient.refetchQueries({ type: 'active' })
// Refetch all active queries that begin with `posts` in the key
-await queryClient.refetchQueries('posts', { active: true })
+await queryClient.refetchQueries(['posts'], { type: 'active' })
```
A query filter object supports the following properties:
- `exact?: boolean`
- If you don't want to search queries inclusively by query key, you can pass the `exact: true` option to return only the query with the exact query key you have passed.
-- `active?: boolean`
- - When set to `true` it will match active queries.
- - When set to `false` it will match inactive queries.
-- `inactive?: boolean`
- - When set to `true` it will match inactive queries.
- - When set to `false` it will match active queries.
+- `type?: 'active' | 'inactive' | 'all'`
+ - Defaults to `all`
+ - When set to `active` it will match active queries.
+ - When set to `inactive` it will match inactive queries.
- `stale?: boolean`
- When set to `true` it will match stale queries.
- When set to `false` it will match fresh queries.
-- `fetching?: boolean`
- - When set to `true` it will match queries that are currently fetching.
- - When set to `false` it will match queries that are not fetching.
+- `fetchStatus?: FetchStatus`
+ - When set to `fetching` it will match queries that are currently fetching.
+ - When set to `paused` it will match queries that wanted to fetch, but have been `paused`.
+ - When set to `idle` it will match queries that are not fetching.
- `predicate?: (query: Query) => boolean`
- This predicate function will be called for every single query in the cache and be expected to return truthy for queries that are `found`.
- `queryKey?: QueryKey`
@@ -53,7 +52,7 @@ A mutation filter is an object with certain conditions to match a mutation with:
await queryClient.isMutating()
// Filter mutations by mutationKey
-await queryClient.isMutating({ mutationKey: "post" })
+await queryClient.isMutating({ mutationKey: ["post"] })
// Filter mutations using a predicate function
await queryClient.isMutating({ predicate: (mutation) => mutation.options.variables?.id === 1 })
diff --git a/docs/src/pages/guides/infinite-queries.md b/docs/src/pages/guides/infinite-queries.md
index 50d300bab6..7a5b812265 100644
--- a/docs/src/pages/guides/infinite-queries.md
+++ b/docs/src/pages/guides/infinite-queries.md
@@ -56,7 +56,7 @@ function Projects() {
isFetching,
isFetchingNextPage,
status,
- } = useInfiniteQuery('projects', fetchProjects, {
+ } = useInfiniteQuery(['projects'], fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
@@ -100,7 +100,7 @@ When an infinite query becomes `stale` and needs to be refetched, each group is
If you only want to actively refetch a subset of all pages, you can pass the `refetchPage` function to `refetch` returned from `useInfiniteQuery`.
```js
-const { refetch } = useInfiniteQuery('projects', fetchProjects, {
+const { refetch } = useInfiniteQuery(['projects'], fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
@@ -132,7 +132,7 @@ function Projects() {
isFetchingNextPage,
fetchNextPage,
hasNextPage,
- } = useInfiniteQuery('projects', fetchProjects, {
+ } = useInfiniteQuery(['projects'], fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
@@ -146,7 +146,7 @@ function Projects() {
Bi-directional lists can be implemented by using the `getPreviousPageParam`, `fetchPreviousPage`, `hasPreviousPage` and `isFetchingPreviousPage` properties and functions.
```js
-useInfiniteQuery('projects', fetchProjects, {
+useInfiniteQuery(['projects'], fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
})
@@ -157,7 +157,7 @@ useInfiniteQuery('projects', fetchProjects, {
Sometimes you may want to show the pages in reversed order. If this is case, you can use the `select` option:
```js
-useInfiniteQuery('projects', fetchProjects, {
+useInfiniteQuery(['projects'], fetchProjects, {
select: data => ({
pages: [...data.pages].reverse(),
pageParams: [...data.pageParams].reverse(),
@@ -170,7 +170,7 @@ useInfiniteQuery('projects', fetchProjects, {
Manually removing first page:
```js
-queryClient.setQueryData('projects', data => ({
+queryClient.setQueryData(['projects'], data => ({
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1),
}))
@@ -183,7 +183,7 @@ const newPagesArray = oldPagesArray?.pages.map((page) =>
page.filter((val) => val.id !== updatedId)
) ?? []
-queryClient.setQueryData('projects', data => ({
+queryClient.setQueryData(['projects'], data => ({
pages: newPagesArray,
pageParams: data.pageParams,
}))
diff --git a/docs/src/pages/guides/initial-query-data.md b/docs/src/pages/guides/initial-query-data.md
index 5a3e9ef454..a634756112 100644
--- a/docs/src/pages/guides/initial-query-data.md
+++ b/docs/src/pages/guides/initial-query-data.md
@@ -19,7 +19,7 @@ There may be times when you already have the initial data for a query available
```js
function Todos() {
- const result = useQuery('todos', () => fetch('/todos'), {
+ const result = useQuery(['todos'], () => fetch('/todos'), {
initialData: initialTodos,
})
}
@@ -34,7 +34,7 @@ By default, `initialData` is treated as totally fresh, as if it were just fetche
```js
function Todos() {
// Will show initialTodos immediately, but also immediately refetch todos after mount
- const result = useQuery('todos', () => fetch('/todos'), {
+ const result = useQuery(['todos'], () => fetch('/todos'), {
initialData: initialTodos,
})
}
@@ -45,7 +45,7 @@ By default, `initialData` is treated as totally fresh, as if it were just fetche
```js
function Todos() {
// Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms
- const result = useQuery('todos', () => fetch('/todos'), {
+ const result = useQuery(['todos'], () => fetch('/todos'), {
initialData: initialTodos,
staleTime: 1000,
})
@@ -56,7 +56,7 @@ By default, `initialData` is treated as totally fresh, as if it were just fetche
```js
function Todos() {
// Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms
- const result = useQuery('todos', () => fetch('/todos'), {
+ const result = useQuery(['todos'], () => fetch('/todos'), {
initialData: initialTodos,
staleTime: 60 * 1000 // 1 minute
// This could be 10 seconds ago or 10 minutes ago
@@ -74,7 +74,7 @@ If the process for accessing a query's initial data is intensive or just not som
```js
function Todos() {
- const result = useQuery('todos', () => fetch('/todos'), {
+ const result = useQuery(['todos'], () => fetch('/todos'), {
initialData: () => {
return getExpensiveTodos()
},
@@ -91,7 +91,7 @@ function Todo({ todoId }) {
const result = useQuery(['todo', todoId], () => fetch('/todos'), {
initialData: () => {
// Use a todo from the 'todos' query as the initial data for this todo query
- return queryClient.getQueryData('todos')?.find(d => d.id === todoId)
+ return queryClient.getQueryData(['todos'])?.find(d => d.id === todoId)
},
})
}
@@ -105,9 +105,9 @@ Getting initial data from the cache means the source query you're using to look
function Todo({ todoId }) {
const result = useQuery(['todo', todoId], () => fetch(`/todos/${todoId}`), {
initialData: () =>
- queryClient.getQueryData('todos')?.find(d => d.id === todoId),
+ queryClient.getQueryData(['todos'])?.find(d => d.id === todoId),
initialDataUpdatedAt: () =>
- queryClient.getQueryState('todos')?.dataUpdatedAt,
+ queryClient.getQueryState(['todos'])?.dataUpdatedAt,
})
}
```
@@ -121,7 +121,7 @@ function Todo({ todoId }) {
const result = useQuery(['todo', todoId], () => fetch(`/todos/${todoId}`), {
initialData: () => {
// Get the query state
- const state = queryClient.getQueryState('todos')
+ const state = queryClient.getQueryState(['todos'])
// If the query exists and has data that is no older than 10 seconds...
if (state && Date.now() - state.dataUpdatedAt <= 10 * 1000) {
diff --git a/docs/src/pages/guides/invalidations-from-mutations.md b/docs/src/pages/guides/invalidations-from-mutations.md
index a8000d216a..2aa38f48fb 100644
--- a/docs/src/pages/guides/invalidations-from-mutations.md
+++ b/docs/src/pages/guides/invalidations-from-mutations.md
@@ -21,8 +21,8 @@ const queryClient = useQueryClient()
// When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key
const mutation = useMutation(addTodo, {
onSuccess: () => {
- queryClient.invalidateQueries('todos')
- queryClient.invalidateQueries('reminders')
+ queryClient.invalidateQueries(['todos'])
+ queryClient.invalidateQueries(['reminders'])
},
})
```
diff --git a/docs/src/pages/guides/migrating-to-react-query-3.md b/docs/src/pages/guides/migrating-to-react-query-3.md
index 12aba57b35..31ce396ca3 100644
--- a/docs/src/pages/guides/migrating-to-react-query-3.md
+++ b/docs/src/pages/guides/migrating-to-react-query-3.md
@@ -244,7 +244,7 @@ const {
This allows for easier manipulation of the data and the page params, like, for example, removing the first page of data along with it's params:
```js
-queryClient.setQueryData('projects', data => ({
+queryClient.setQueryData(['projects'], data => ({
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1),
}))
@@ -358,7 +358,7 @@ Only re-render when the `data` or `error` properties change:
import { useQuery } from 'react-query'
function User() {
- const { data } = useQuery('user', fetchUser, {
+ const { data } = useQuery(['user'], fetchUser, {
notifyOnChangeProps: ['data', 'error'],
})
return
Username: {data.username}
@@ -371,7 +371,7 @@ Prevent re-render when the `isStale` property changes:
import { useQuery } from 'react-query'
function User() {
- const { data } = useQuery('user', fetchUser, {
+ const { data } = useQuery(['user'], fetchUser, {
notifyOnChangePropsExclusions: ['isStale'],
})
return
Username: {data.username}
@@ -459,7 +459,7 @@ The `useQuery` and `useInfiniteQuery` hooks now have a `select` option to select
import { useQuery } from 'react-query'
function User() {
- const { data } = useQuery('user', fetchUser, {
+ const { data } = useQuery(['user'], fetchUser, {
select: user => user.username,
})
return
Username: {data}
@@ -556,10 +556,10 @@ const unsubscribe = observer.subscribe(result => {
The `QueryClient.setQueryDefaults()` method can be used to set default options for specific queries:
```js
-queryClient.setQueryDefaults('posts', { queryFn: fetchPosts })
+queryClient.setQueryDefaults(['posts'], { queryFn: fetchPosts })
function Component() {
- const { data } = useQuery('posts')
+ const { data } = useQuery(['posts'])
}
```
@@ -568,10 +568,10 @@ function Component() {
The `QueryClient.setMutationDefaults()` method can be used to set default options for specific mutations:
```js
-queryClient.setMutationDefaults('addPost', { mutationFn: addPost })
+queryClient.setMutationDefaults(['addPost'], { mutationFn: addPost })
function Component() {
- const { mutate } = useMutation('addPost')
+ const { mutate } = useMutation(['addPost'])
}
```
diff --git a/docs/src/pages/guides/migrating-to-react-query-4.md b/docs/src/pages/guides/migrating-to-react-query-4.md
new file mode 100644
index 0000000000..14002bec89
--- /dev/null
+++ b/docs/src/pages/guides/migrating-to-react-query-4.md
@@ -0,0 +1,255 @@
+---
+id: migrating-to-react-query-4
+title: Migrating to React Query 4
+---
+
+## Breaking Changes
+
+### Query Keys (and Mutation Keys) need to be an Array
+
+In v3, Query and Mutation Keys could be a String or an Array. Internally, React Query has always worked with Array Keys only, and we've sometimes exposed this to consumers. For example, in the `queryFn`, you would always get the key as an Array to make working with [Default Query Functions](./default-query-function) easier.
+
+However, we have not followed this concept through to all apis. For example, when using the `predicate` function on [Query Filters](./filters) you would get the raw Query Key. This makes it difficult to work with such functions if you use Query Keys that are mixed Arrays and Strings. The same was true when using global callbacks.
+
+To streamline all apis, we've decided to make all keys Arrays only:
+
+```diff
+- useQuery('todos', fetchTodos)
++ useQuery(['todos'], fetchTodos)
+```
+
+### Separate hydration exports have been removed
+
+With version [3.22.0](https://github.com/tannerlinsley/react-query/releases/tag/v3.22.0), hydration utilities moved into the React Query core. With v3, you could still use the old exports from `react-query/hydration`, but these exports have been removed with v4.
+
+```diff
+- import { dehydrate, hydrate, useHydrate, Hydrate } from 'react-query/hydration'
++ import { dehydrate, hydrate, useHydrate, Hydrate } from 'react-query'
+```
+
+### `notifyOnChangeProps` property no longer accepts `"tracked"` as a value
+
+The `notifyOnChangeProps` option no longer accepts a `"tracked"` value. Instead, `useQuery` defaults to tracking properties. All queries using `notifyOnChangeProps: "tracked"` should be updated by removing this option.
+
+If you would like to bypass this in any queries to emulate the v3 default behavior of re-rendering whenever a query changes, `notifyOnChangeProps` now accepts an `"all"` value to opt-out of the default smart tracking optimization.
+
+### `notifyOnChangePropsExclusion` has been removed
+
+In v4, `notifyOnChangeProps` defaults to the `"tracked"` behavior of v3 instead of `undefined`. Now that `"tracked"` is the default behavior for v4, it no longer makes sense to include this config option.
+
+### Consistent behavior for `cancelRefetch`
+
+The `cancelRefetch` can be passed to all functions that imperatively fetch a query, namely:
+
+- `queryClient.refetchQueries`
+ - `queryClient.invalidateQueries`
+ - `queryClient.resetQueries`
+- `refetch` returned from `useQuery`
+- `fetchNetPage` and `fetchPreviousPage` returned from `useInfiniteQuery`
+
+Except for `fetchNetxPage` and `fetchPreviousPage`, this flag was defaulting to `false`, which was inconsistent and potentially troublesome: Calling `refetchQueries` or `invalidateQueries` after a mutation might not yield the latest result if a previous slow fetch was already ongoing, because this refetch would have been skipped.
+
+We believe that if a query is actively refetched by some code you write, it should, per default, re-start the fetch.
+
+That is why this flag now defaults to _true_ for all methods mentioned above. It also means that if you call `refetchQueries` twice in a row, without awaiting it, it will now cancel the first fetch and re-start it with the second one:
+
+```
+queryClient.refetchQueries({ queryKey: ['todos'] })
+// this will abort the previous refetch and start a new fetch
+queryClient.refetchQueries({ queryKey: ['todos'] })
+```
+
+You can opt-out of this behaviour by explicitly passing `cancelRefetch:false`:
+
+```
+queryClient.refetchQueries({ queryKey: ['todos'] })
+// this will not abort the previous refetch - it will just be ignored
+queryClient.refetchQueries({ queryKey: ['todos'] }, { cancelRefetch: false })
+```
+
+> Note: There is no change in behaviour for automatically triggered fetches, e.g. because a query mounts or because of a window focus refetch.
+
+### Query Filters
+
+A [query filter](../guides/filters) is an object with certain conditions to match a query. Historically, the filter options have mostly been a combination of boolean flags. However, combining those flags can lead to impossible states. Specifically:
+
+```
+active?: boolean
+ - When set to true it will match active queries.
+ - When set to false it will match inactive queries.
+inactive?: boolean
+ - When set to true it will match inactive queries.
+ - When set to false it will match active queries.
+```
+
+Those flags don't work well when used together, because they are mutually exclusive. Setting `false` for both flags could match all queries, judging from the description, or no queries, which doesn't make much sense.
+
+With v4, those filters have been combined into a single filter to better show the intent:
+
+```diff
+- active?: boolean
+- inactive?: boolean
++ type?: 'active' | 'inactive' | 'all'
+```
+
+The filter defaults to `all`, and you can choose to only match `active` or `inactive` queries.
+
+#### refetchActive / refetchInactive
+
+[queryClient.invalidateQueries](../reference/QueryClient#queryclientinvalidatequeries) had two additional, similar flags:
+
+```
+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
+```
+
+For the same reason, those have also been combined:
+
+```diff
+- active?: boolean
+- inactive?: boolean
++ refetchType?: 'active' | 'inactive' | 'all' | 'none'
+```
+
+This flag defaults to `active` because `refetchActive` defaulted to `true`. This means we also need a way to tell `invalidateQueries` to not refetch at all, which is why a fourth option (`none`) is also allowed here.
+
+### Streamlined NotifyEvents
+
+Subscribing manually to the `QueryCache` has always given you a `QueryCacheNotifyEvent`, but this was not true for the `MutationCache`. We have streamlined the behavior and also adapted event names accordingly.
+
+#### QueryCacheNotifyEvent
+
+```diff
+- type: 'queryAdded'
++ type: 'added'
+- type: 'queryRemoved'
++ type: 'removed'
+- type: 'queryUpdated'
++ type: 'updated'
+```
+
+#### MutationCacheNotifyEvent
+
+The `MutationCacheNotifyEvent` uses the same types as the `QueryCacheNotifyEvent`.
+
+> Note: This is only relevant if you manually subscribe to the caches via `queryCache.subscribe` or `mutationCache.subscribe`
+
+### The `src/react` directory was renamed to `src/reactjs`
+
+Previously, React Query had a directory named `react` which imported from the `react` module. This could cause problems with some Jest configurations, resulting in errors when running tests like:
+
+```
+TypeError: Cannot read property 'createContext' of undefined
+```
+
+With the renamed directory this no longer is an issue.
+
+If you were importing anything from `'react-query/react'` directly in your project (as opposed to just `'react-query'`), then you need to update your imports:
+
+```diff
+- import { QueryClientProvider } from 'react-query/react';
++ import { QueryClientProvider } from 'react-query/reactjs';
+```
+
+### `onSuccess` is no longer called from `setQueryData`
+
+This was confusing to many and also created infinite loops if `setQueryData` was called from within `onSuccess`. It was also a frequent source of error when combined with `staleTime`, because if data was read from the cache only, `onSuccess` was _not_ called.
+
+Similar to `onError` and `onSettled`, the `onSuccess` callback is now tied to a request being made. No request -> no callback.
+
+If you want to listen to changes of the `data` field, you can best do this with a `useEffect`, where `data` is part of the dependency Array. Since React Query ensures stable data through structural sharing, the effect will not execute with every background refetch, but only if something within data has changed:
+
+```
+const { data } = useQuery({ queryKey, queryFn })
+React.useEffect(() => mySideEffectHere(data), [data])
+```
+
+### `persistQueryClient` and the corresponding persister plugins are no longer experimental and have been renamed
+
+The plugins `createWebStoragePersistor` and `createAsyncStoragePersistor` have been renamed to [`createWebStoragePersister`](/plugins/createWebStoragePersister) and [`createAsyncStoragePersister`](/plugins/createAsyncStoragePersister) respectively. The interface `Persistor` in `persistQueryClient` has also been renamed to `Persister`. Checkout [this stackexchange](https://english.stackexchange.com/questions/206893/persister-or-persistor) for the motivation of this change.
+
+Since these plugins are no longer experimental, their import paths have also been updated:
+
+```diff
+- import { persistQueryClient } from 'react-query/persistQueryClient-experimental'
+- import { createWebStoragePersistor } from 'react-query/createWebStoragePersistor-experimental'
+- import { createAsyncStoragePersistor } from 'react-query/createAsyncStoragePersistor-experimental'
+
++ import { persistQueryClient } from 'react-query/persistQueryClient'
++ import { createWebStoragePersister } from 'react-query/createWebStoragePersister'
++ import { createAsyncStoragePersister } from 'react-query/createAsyncStoragePersister'
+```
+
+### The `cancel` method on promises is no longer supported
+
+The [old `cancel` method](../guides/query-cancellation#old-cancel-function) that allowed you to define a `cancel` function on promises, which was then used by the library to support query cancellation, has been removed. We recommend to use the [newer API](../guides/query-cancellation) (introduced with v3.30.0) for query cancellation that uses the [`AbortController` API](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) internally and provides you with an [`AbortSignal` instance](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) for your query function to support query cancellation.
+
+### Queries and mutations, per default, need network connection to run
+
+Please read the [New Features announcement](#proper-offline-support) about online / offline support, and also the dedicated page about [Network mode](../guides/network-mode)
+
+Even though React Query is an Async State Manager that can be used for anything that produces a Promise, it is most often used for data fetching in combination with data fetching libraries. That is why, per default, queries and mutations will be `paused` if there is no network connection. If you want to opt-in to the previous behavior, you can globally set `networkMode: offlineFirst` for both queries and mutations:
+
+```js
+new QueryClient({
+ defaultOptions: {
+ queries: {
+ networkMode: 'offlineFirst'
+ },
+ mutations: {
+ networkmode: 'offlineFirst'
+ }
+ }
+})
+```
+
+### Removed undocumented methods from the `queryClient`
+
+The methods `cancelMutatations` and `executeMutation` were undocumented and unused internally, so we removed them. Since they were just wrappers around methods available on the `mutationCache`, you can still use the functionality.
+
+```diff
+- cancelMutations(): Promise {
+- const promises = notifyManager.batch(() =>
+- this.mutationCache.getAll().map(mutation => mutation.cancel())
+- )
+- return Promise.all(promises).then(noop).catch(noop)
+- }
+```
+
+```diff
+- executeMutation<
+- TData = unknown,
+- TError = unknown,
+- TVariables = void,
+- TContext = unknown
+- >(
+- options: MutationOptions
+- ): Promise {
+- return this.mutationCache.build(this, options).execute()
+- }
+```
+
+## New Features 🚀
+
+### Proper offline support
+
+In v3, React Query has always fired off queries and mutations, but then taken the assumption that if you want to retry it, you need to be connected to the internet. This has led to several confusing situations:
+
+- You are offline and mount a query - it goes to loading state, the request fails, and it stays in loading state until you go online again, even though it is not really fetching.
+- Similarly, if you are offline and have retries turned off, your query will just fire and fail, and the query goes to error state.
+- You are offline and want to fire off a query that doesn't necessarily need network connection (because you _can_ use React Query for something other than data fetching), but it fails for some other reason. That query will now be paused until you go online again.
+- Window focus refetching didn't do anything at all if you were offline.
+
+With v4, React Query introduces a new `networkMode` to tackle all these issues. Please read the dedicated page about the new [Network mode](../guides/network-mode) for more information.
+
+### Mutation Cache Garbage Collection
+
+Mutations can now also be garbage collected automatically, just like queries. The default `cacheTime` for mutations is also set to 5 minutes.
+
+### Tracked Queries per default
+
+React Query defaults to "tracking" query properties, which should give you a nice boost in render optimization. The feature has existed since [v3.6.0](https://github.com/tannerlinsley/react-query/releases/tag/v3.6.0) and has now become the default behavior with v4.
diff --git a/docs/src/pages/guides/mutations.md b/docs/src/pages/guides/mutations.md
index 379d7351ef..f592e0dc17 100644
--- a/docs/src/pages/guides/mutations.md
+++ b/docs/src/pages/guides/mutations.md
@@ -216,34 +216,34 @@ Mutations can be persisted to storage if needed and resumed at a later point. Th
const queryClient = new QueryClient()
// Define the "addTodo" mutation
-queryClient.setMutationDefaults('addTodo', {
+queryClient.setMutationDefaults(['addTodo'], {
mutationFn: addTodo,
onMutate: async (variables) => {
// Cancel current queries for the todos list
- await queryClient.cancelQueries('todos')
+ await queryClient.cancelQueries(['todos'])
// Create optimistic todo
const optimisticTodo = { id: uuid(), title: variables.title }
// Add optimistic todo to todos list
- queryClient.setQueryData('todos', old => [...old, optimisticTodo])
+ queryClient.setQueryData(['todos'], old => [...old, optimisticTodo])
// Return context with the optimistic todo
return { optimisticTodo }
},
onSuccess: (result, variables, context) => {
// Replace optimistic todo in the todos list with the result
- queryClient.setQueryData('todos', old => old.map(todo => todo.id === context.optimisticTodo.id ? result : todo))
+ queryClient.setQueryData(['todos'], old => old.map(todo => todo.id === context.optimisticTodo.id ? result : todo))
},
onError: (error, variables, context) => {
// Remove optimistic todo from the todos list
- queryClient.setQueryData('todos', old => old.filter(todo => todo.id !== context.optimisticTodo.id))
+ queryClient.setQueryData(['todos'], old => old.filter(todo => todo.id !== context.optimisticTodo.id))
},
retry: 3,
})
// Start mutation in some component:
-const mutation = useMutation('addTodo')
+const mutation = useMutation(['addTodo'])
mutation.mutate({ title: 'title' })
// If the mutation has been paused because the device is for example offline,
diff --git a/docs/src/pages/guides/network-mode.md b/docs/src/pages/guides/network-mode.md
new file mode 100644
index 0000000000..3a977ff2d2
--- /dev/null
+++ b/docs/src/pages/guides/network-mode.md
@@ -0,0 +1,46 @@
+---
+id: network-mode
+title: Network Mode
+---
+
+React Query provides three different network modes to distinguish how [Queries](./queries) and [Mutations](./mutations) should behave if you have no network connection. This mode can be set for each Query / Mutation individually, or globally via the query / mutation defaults.
+
+Since React Query is most often used for data fetching in combination with data fetching libraries, the default network mode is [online](#network-mode-online).
+
+## Network Mode: online
+
+In this mode, Queries and Mutations will not fire unless you have network connection. This is the default mode. If a fetch is initiated for a query, it will always stay in the `state` (`loading`, `idle`, `error`, `success`) it is in if the fetch cannot be made because there is no network connection. However, a `fetchStatus` is exposed additionally. This can be either:
+
+- `fetching`: The `queryFn` is really executing - a request is in-flight.
+- `paused`: The query is not executing - it is `paused` until you have connection again
+- `idle`: The query is not fetching and not paused
+
+The flags `isFetching` and `isPaused` are derived from this state and exposed for convenience.
+
+> Keep in mind that it might not be enough to check for `loading` state to show a loading spinner. Queries can be in `state: 'loading'`, but `fetchStatus: 'paused'` if they are mounting for the first time, and you have no network connection.
+
+If a query runs because you are online, but you go offline while the fetch is still happening, React Query will also pause the retry mechanism. Paused queries will then continue to run once you re-gain network connection. This is independent of `refetchOnReconnect` (which also defaults to `true` in this mode), because it is not a `refetch`, but rather a `continue`. If the query has been [cancelled](./query-cancellation) in the meantime, it will not continue.
+
+## Network Mode: always
+
+In this mode, React Query will always fetch and ignore the online / offline state. This is likely the mode you want to choose if you use React Query in an environment where you don't need an active network connection for your Queries to work - e.g. if you just read from `AsyncStorage`, or if you just want to return `Promise.resolve(5)` from your `queryFn`.
+
+- Queries will never be `paused` because you have no network connection.
+- Retries will also not pause - your Query will go to `error` state if it fails.
+- `refetchOnReconnect` defaults to `false` in this mode, because reconnecting to the network is not a good indicator anymore that stale queries should be refetched. You can still turn it on if you want.
+
+## Network Mode: offlineFirst
+
+This mode is the middle ground between the first two options, where React Query will run the `queryFn` once, but then pause retries. This is very handy if you have a serviceWorker that intercepts a request for caching like in an [offline-first PWA](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Offline_Service_workers), or if you use HTTP caching via the [Cache-Control header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#the_cache-control_header).
+
+In those situations, the first fetch might succeed because it comes from an offline storage / cache. However, if there is a cache miss, the network request will go out and fail, in which case this mode behaves like an `online` query - pausing retries.
+
+## Devtools
+
+The [React Query Devtools](../devtools) will show Queries in a `paused` state if they would be fetching, but there is no network connection. There is also a toggle button to _Mock offline behavior_. Please note that this button will _not_ actually mess with your network connection (you can do that in the browser devtools), but it will set the [OnlineManager](../reference/onlineManager) in an offline state.
+
+## Signature
+
+- `networkMode: 'online' | 'always' | 'offlineFirst`
+ - optional
+ - defaults to `'online'`
diff --git a/docs/src/pages/guides/optimistic-updates.md b/docs/src/pages/guides/optimistic-updates.md
index b4bd3b4080..31518b3434 100644
--- a/docs/src/pages/guides/optimistic-updates.md
+++ b/docs/src/pages/guides/optimistic-updates.md
@@ -16,24 +16,24 @@ 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')
+ await queryClient.cancelQueries(['todos'])
// Snapshot the previous value
- const previousTodos = queryClient.getQueryData('todos')
+ const previousTodos = queryClient.getQueryData(['todos'])
// Optimistically update to the new value
- queryClient.setQueryData('todos', old => [...old, newTodo])
+ 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)
+ queryClient.setQueryData(['todos'], context.previousTodos)
},
// Always refetch after error or success:
onSettled: () => {
- queryClient.invalidateQueries('todos')
+ queryClient.invalidateQueries(['todos'])
},
})
```
diff --git a/docs/src/pages/guides/parallel-queries.md b/docs/src/pages/guides/parallel-queries.md
index 22926ee6e0..79dac5ce82 100644
--- a/docs/src/pages/guides/parallel-queries.md
+++ b/docs/src/pages/guides/parallel-queries.md
@@ -12,9 +12,9 @@ When the number of parallel queries does not change, there is **no extra effort*
```js
function App () {
// The following queries will execute in parallel
- const usersQuery = useQuery('users', fetchUsers)
- const teamsQuery = useQuery('teams', fetchTeams)
- const projectsQuery = useQuery('projects', fetchProjects)
+ const usersQuery = useQuery(['users'], fetchUsers)
+ const teamsQuery = useQuery(['teams'], fetchTeams)
+ const projectsQuery = useQuery(['projects'], fetchProjects)
...
}
```
diff --git a/docs/src/pages/guides/placeholder-query-data.md b/docs/src/pages/guides/placeholder-query-data.md
index e9c2d24379..678197fa64 100644
--- a/docs/src/pages/guides/placeholder-query-data.md
+++ b/docs/src/pages/guides/placeholder-query-data.md
@@ -20,7 +20,7 @@ There are a few ways to supply placeholder data for a query to the cache before
```js
function Todos() {
- const result = useQuery('todos', () => fetch('/todos'), {
+ const result = useQuery(['todos'], () => fetch('/todos'), {
placeholderData: placeholderTodos,
})
}
@@ -33,7 +33,7 @@ If the process for accessing a query's placeholder data is intensive or just not
```js
function Todos() {
const placeholderData = useMemo(() => generateFakeTodos(), [])
- const result = useQuery('todos', () => fetch('/todos'), { placeholderData })
+ const result = useQuery(['todos'], () => fetch('/todos'), { placeholderData })
}
```
@@ -47,7 +47,7 @@ function Todo({ blogPostId }) {
placeholderData: () => {
// Use the smaller/preview version of the blogPost from the 'blogPosts' query as the placeholder data for this blogPost query
return queryClient
- .getQueryData('blogPosts')
+ .getQueryData(['blogPosts'])
?.find(d => d.id === blogPostId)
},
})
diff --git a/docs/src/pages/guides/prefetching.md b/docs/src/pages/guides/prefetching.md
index 72ffdcd5e8..d1f04858cd 100644
--- a/docs/src/pages/guides/prefetching.md
+++ b/docs/src/pages/guides/prefetching.md
@@ -8,7 +8,7 @@ If you're lucky enough, you may know enough about what your users will do to be
```js
const prefetchTodos = async () => {
// The results of this query will be cached like a normal query
- await queryClient.prefetchQuery('todos', fetchTodos)
+ await queryClient.prefetchQuery(['todos'], fetchTodos)
}
```
@@ -21,5 +21,5 @@ const prefetchTodos = async () => {
Alternatively, if you already have the data for your query synchronously available, you don't need to prefetch it. You can just use the [Query Client's `setQueryData` method](../reference/QueryClient#queryclientsetquerydata) to directly add or update a query's cached result by key.
```js
-queryClient.setQueryData('todos', todos)
+queryClient.setQueryData(['todos'], todos)
```
diff --git a/docs/src/pages/guides/queries.md b/docs/src/pages/guides/queries.md
index 63c7b5eb7d..af83df3ba3 100644
--- a/docs/src/pages/guides/queries.md
+++ b/docs/src/pages/guides/queries.md
@@ -18,7 +18,7 @@ To subscribe to a query in your components or custom hooks, call the `useQuery`
import { useQuery } from 'react-query'
function App() {
- const info = useQuery('todos', fetchTodoList)
+ const info = useQuery(['todos'], fetchTodoList)
}
```
@@ -27,7 +27,7 @@ The **unique key** you provide is used internally for refetching, caching, and s
The query results returned by `useQuery` contains all of the information about the query that you'll need for templating and any other usage of the data:
```js
-const result = useQuery('todos', fetchTodoList)
+const result = useQuery(['todos'], fetchTodoList)
```
The `result` object contains a few very important states you'll need to be aware of to be productive. A query can only be in one of the following states at any given moment:
@@ -47,7 +47,7 @@ For **most** queries, it's usually sufficient to check for the `isLoading` state
```js
function Todos() {
- const { isLoading, isError, data, error } = useQuery('todos', fetchTodoList)
+ const { isLoading, isError, data, error } = useQuery(['todos'], fetchTodoList)
if (isLoading) {
return Loading...
@@ -72,7 +72,7 @@ If booleans aren't your thing, you can always use the `status` state as well:
```js
function Todos() {
- const { status, data, error } = useQuery('todos', fetchTodoList)
+ const { status, data, error } = useQuery(['todos'], fetchTodoList)
if (status === 'loading') {
return Loading...
diff --git a/docs/src/pages/guides/query-cancellation.md b/docs/src/pages/guides/query-cancellation.md
index 2c00fe1625..0d36fced6e 100644
--- a/docs/src/pages/guides/query-cancellation.md
+++ b/docs/src/pages/guides/query-cancellation.md
@@ -20,7 +20,7 @@ However, if you consume the `AbortSignal` or attach a `cancel` function to your
## Using `fetch`
```js
-const query = useQuery('todos', async ({ signal }) => {
+const query = useQuery(['todos'], async ({ signal }) => {
const todosResponse = await fetch('/todos', {
// Pass the signal to one fetch
signal,
@@ -46,7 +46,7 @@ const query = useQuery('todos', async ({ signal }) => {
```js
import axios from 'axios'
-const query = useQuery('todos', ({ signal }) =>
+const query = useQuery(['todos'], ({ signal }) =>
axios.get('/todos', {
// Pass the signal to `axios`
signal,
@@ -59,7 +59,7 @@ const query = useQuery('todos', ({ signal }) =>
```js
import axios from 'axios'
-const query = useQuery('todos', ({ signal }) => {
+const query = useQuery(['todos'], ({ signal }) => {
// Create a new CancelToken source for this request
const CancelToken = axios.CancelToken
const source = CancelToken.source()
@@ -81,7 +81,7 @@ const query = useQuery('todos', ({ signal }) => {
## Using `XMLHttpRequest`
```js
-const query = useQuery('todos', ({ signal }) => {
+const query = useQuery(['todos'], ({ signal }) => {
return new Promise((resolve, reject) => {
var oReq = new XMLHttpRequest()
oReq.addEventListener('load', () => {
@@ -102,7 +102,7 @@ const query = useQuery('todos', ({ signal }) => {
An `AbortSignal` can be set in the `GraphQLClient` constructor.
```js
-const query = useQuery('todos', ({ signal }) => {
+const query = useQuery(['todos'], ({ signal }) => {
const client = new GraphQLClient(endpoint, {
signal,
});
@@ -115,9 +115,7 @@ const query = useQuery('todos', ({ signal }) => {
You might want to cancel a query manually. For example, if the request takes a long time to finish, you can allow the user to click a cancel button to stop the request. To do this, you just need to call `queryClient.cancelQueries(key)`, which will cancel the query and revert it back to its previous state. If `promise.cancel` is available, or you have consumed the `signal` passed to the query function, React Query will additionally also cancel the Promise.
```js
-const [queryKey] = useState('todos')
-
-const query = useQuery(queryKey, await ({ signal }) => {
+const query = useQuery(['todos'], await ({ signal }) => {
const resp = fetch('/todos', { signal })
return resp.json()
})
@@ -127,7 +125,7 @@ const queryClient = useQueryClient()
return (
)
```
@@ -143,7 +141,7 @@ To integrate with this feature, attach a `cancel` function to the promise return
```js
import axios from 'axios'
-const query = useQuery('todos', () => {
+const query = useQuery(['todos'], () => {
// Create a new CancelToken source for this request
const CancelToken = axios.CancelToken
const source = CancelToken.source()
@@ -165,7 +163,7 @@ const query = useQuery('todos', () => {
## Using `fetch` with `cancel` function
```js
-const query = useQuery('todos', () => {
+const query = useQuery(['todos'], () => {
// Create a new AbortController instance for this request
const controller = new AbortController()
// Get the abortController's signal
diff --git a/docs/src/pages/guides/query-functions.md b/docs/src/pages/guides/query-functions.md
index e31eb25489..a12d84e0b7 100644
--- a/docs/src/pages/guides/query-functions.md
+++ b/docs/src/pages/guides/query-functions.md
@@ -47,7 +47,7 @@ useQuery(['todos', todoId], async () => {
## Query Function Variables
-Query keys are not just for uniquely identifying the data you are fetching, but are also conveniently passed into your query function and while not always necessary, this makes it possible to extract your query functions if needed:
+Query keys are not just for uniquely identifying the data you are fetching, but are also conveniently passed into your query function as part of the QueryFunctionContext. While not always necessary, this makes it possible to extract your query functions if needed:
```js
function Todos({ status, page }) {
@@ -61,6 +61,20 @@ function fetchTodoList({ queryKey }) {
}
```
+### QueryFunctionContext
+
+The `QueryFunctionContext` is the object passed to each query function. It consists of:
+
+- `queryKey: QueryKey`: [Query Keys](./query-keys)
+- `pageParam: unknown | undefined`
+ - only for [Infinite Queries](./infinite-queries.md)
+ - the page parameter used to fetch the current page
+- signal?: AbortSignal
+ - [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) instance provided by react-query
+ - Can be used for [Query Cancellation](./query-cancellation.md)
+- `meta?: Record`
+ - an optional field you can fill with additional information about your query
+
## Using a Query Object instead of parameters
Anywhere the `[queryKey, queryFn, config]` signature is supported throughout React Query's API, you can also use an object to express the same configuration:
diff --git a/docs/src/pages/guides/query-invalidation.md b/docs/src/pages/guides/query-invalidation.md
index 2f6b2b6789..17f0d486a1 100644
--- a/docs/src/pages/guides/query-invalidation.md
+++ b/docs/src/pages/guides/query-invalidation.md
@@ -9,7 +9,7 @@ Waiting for queries to become stale before they are fetched again doesn't always
// Invalidate every query in the cache
queryClient.invalidateQueries()
// Invalidate every query with a key that starts with `todos`
-queryClient.invalidateQueries('todos')
+queryClient.invalidateQueries(['todos'])
```
> Note: Where other libraries that use normalized caches would attempt to update local queries with the new data either imperatively or via schema inference, React Query gives you the tools to avoid the manual labor that comes with maintaining normalized caches and instead prescribes **targeted invalidation, background-refetching and ultimately atomic updates**.
@@ -31,10 +31,10 @@ import { useQuery, useQueryClient } from 'react-query'
// Get QueryClient from the context
const queryClient = useQueryClient()
-queryClient.invalidateQueries('todos')
+queryClient.invalidateQueries(['todos'])
// Both queries below will be invalidated
-const todoListQuery = useQuery('todos', fetchTodoList)
+const todoListQuery = useQuery(['todos'], fetchTodoList)
const todoListQuery = useQuery(['todos', { page: 1 }], fetchTodoList)
```
@@ -47,13 +47,13 @@ queryClient.invalidateQueries(['todos', { type: 'done' }])
const todoListQuery = useQuery(['todos', { type: 'done' }], fetchTodoList)
// However, the following query below will NOT be invalidated
-const todoListQuery = useQuery('todos', fetchTodoList)
+const todoListQuery = useQuery(['todos'], fetchTodoList)
```
The `invalidateQueries` API is very flexible, so even if you want to **only** invalidate `todos` queries that don't have any more variables or subkeys, you can pass an `exact: true` option to the `invalidateQueries` method:
```js
-queryClient.invalidateQueries('todos', { exact: true })
+queryClient.invalidateQueries(['todos'], { exact: true })
// The query below will be invalidated
const todoListQuery = useQuery(['todos'], fetchTodoList)
diff --git a/docs/src/pages/guides/query-keys.md b/docs/src/pages/guides/query-keys.md
index f9239ea91a..fab84b4c2c 100644
--- a/docs/src/pages/guides/query-keys.md
+++ b/docs/src/pages/guides/query-keys.md
@@ -3,24 +3,24 @@ id: query-keys
title: Query Keys
---
-At its core, React Query manages query caching for you based on query keys. Query keys can be as simple as a string, or as complex as an array of many strings and nested objects. As long as the query key is serializable, and **unique to the query's data**, you can use it!
+At its core, React Query manages query caching for you based on query keys. Query keys have to be an Array at the top level, and can be as simple as an Array with a single string, or as complex as an array of many strings and nested objects. As long as the query key is serializable, and **unique to the query's data**, you can use it!
-## String-Only Query Keys
+## Simple Query Keys
-The simplest form of a key is actually not an array, but an individual string. When a string query key is passed, it is converted to an array internally with the string as the only item in the query key. This format is useful for:
+The simplest form of a key is an array with constants values. This format is useful for:
- Generic List/Index resources
- Non-hierarchical resources
```js
// A list of todos
-useQuery('todos', ...) // queryKey === ['todos']
+useQuery(['todos'], ...)
// Something else, whatever!
-useQuery('somethingSpecial', ...) // queryKey === ['somethingSpecial']
+useQuery(['something', 'special'], ...)
```
-## Array Keys
+## Array Keys with variables
When a query needs more information to uniquely describe its data, you can use an array with a string and any number of serializable objects to describe it. This is useful for:
@@ -32,15 +32,12 @@ When a query needs more information to uniquely describe its data, you can use a
```js
// An individual todo
useQuery(['todo', 5], ...)
-// queryKey === ['todo', 5]
// An individual todo in a "preview" format
useQuery(['todo', 5, { preview: true }], ...)
-// queryKey === ['todo', 5, { preview: true }]
// A list of todos that are "done"
useQuery(['todos', { type: 'done' }], ...)
-// queryKey === ['todos', { type: 'done' }]
```
## Query Keys are hashed deterministically!
diff --git a/docs/src/pages/guides/query-retries.md b/docs/src/pages/guides/query-retries.md
index 8b87e5b1f5..df44db6920 100644
--- a/docs/src/pages/guides/query-retries.md
+++ b/docs/src/pages/guides/query-retries.md
@@ -47,7 +47,7 @@ function App() {
Though it is not recommended, you can obviously override the `retryDelay` function/integer in both the Provider and individual query options. If set to an integer instead of a function the delay will always be the same amount of time:
```js
-const result = useQuery('todos', fetchTodoList, {
+const result = useQuery(['todos'], fetchTodoList, {
retryDelay: 1000, // Will always wait 1000ms to retry, regardless of how many retries
})
```
diff --git a/docs/src/pages/guides/ssr.md b/docs/src/pages/guides/ssr.md
index 623b60161e..675551927f 100644
--- a/docs/src/pages/guides/ssr.md
+++ b/docs/src/pages/guides/ssr.md
@@ -31,7 +31,7 @@ export async function getStaticProps() {
}
function Posts(props) {
- const { data } = useQuery('posts', getPosts, { initialData: props.posts })
+ const { data } = useQuery(['posts'], getPosts, { initialData: props.posts })
// ...
}
@@ -95,11 +95,11 @@ export async function getStaticProps() {
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)
+ 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)
+ const { data: otherData } = useQuery(['posts-2'], getPosts)
// ...
}
@@ -126,9 +126,9 @@ This guide is at-best, a high level overview of how SSR with React Query should
```js
import { dehydrate, Hydrate, QueryClient, QueryClientProvider } from 'react-query';
-function handleRequest (req, res) {
+async function handleRequest (req, res) {
const queryClient = new QueryClient()
- await queryClient.prefetchQuery('key', fn)
+ await queryClient.prefetchQuery(['key'], fn)
const dehydratedState = dehydrate(queryClient)
const html = ReactDOM.renderToString(
diff --git a/docs/src/pages/guides/testing.md b/docs/src/pages/guides/testing.md
index 892fadf5b4..bb7b352484 100644
--- a/docs/src/pages/guides/testing.md
+++ b/docs/src/pages/guides/testing.md
@@ -21,7 +21,7 @@ Once installed, a simple test can be written. Given the following custom hook:
```
export function useCustomHook() {
- return useQuery('customHook', () => 'Hello');
+ return useQuery(['customHook'], () => 'Hello');
}
```
@@ -99,7 +99,7 @@ Given the following custom hook:
```
function useFetchData() {
- return useQuery('fetchData', () => request('/api/data'));
+ return useQuery(['fetchData'], () => request('/api/data'));
}
```
diff --git a/docs/src/pages/guides/window-focus-refetching.md b/docs/src/pages/guides/window-focus-refetching.md
index dc3b757d3c..0058831fec 100644
--- a/docs/src/pages/guides/window-focus-refetching.md
+++ b/docs/src/pages/guides/window-focus-refetching.md
@@ -25,7 +25,7 @@ function App() {
#### Disabling Per-Query
```js
-useQuery('todos', fetchTodos, { refetchOnWindowFocus: false })
+useQuery(['todos'], fetchTodos, { refetchOnWindowFocus: false })
```
## Custom Window Focus Event
diff --git a/docs/src/pages/overview.md b/docs/src/pages/overview.md
index 885e29a6a2..5e32e0a240 100644
--- a/docs/src/pages/overview.md
+++ b/docs/src/pages/overview.md
@@ -60,7 +60,7 @@ export default function App() {
}
function Example() {
- const { isLoading, error, data } = useQuery('repoData', () =>
+ const { isLoading, error, data } = useQuery(['repoData'], () =>
fetch('https://api.github.com/repos/tannerlinsley/react-query').then(res =>
res.json()
)
diff --git a/docs/src/pages/plugins/createAsyncStoragePersistor.md b/docs/src/pages/plugins/createAsyncStoragePersister.md
similarity index 62%
rename from docs/src/pages/plugins/createAsyncStoragePersistor.md
rename to docs/src/pages/plugins/createAsyncStoragePersister.md
index 99f2a2d5f9..cdd8650b64 100644
--- a/docs/src/pages/plugins/createAsyncStoragePersistor.md
+++ b/docs/src/pages/plugins/createAsyncStoragePersister.md
@@ -1,25 +1,23 @@
---
-id: createAsyncStoragePersistor
-title: createAsyncStoragePersistor (Experimental)
+id: createAsyncStoragePersister
+title: createAsyncStoragePersister
---
-> VERY IMPORTANT: This utility is currently in an experimental stage. This means that breaking changes will happen in minor AND patch releases. Use at your own risk. If you choose to rely on this in production in an experimental stage, please lock your version to a patch-level version to avoid unexpected breakages.
-
## Installation
-This utility comes packaged with `react-query` and is available under the `react-query/createAsyncStoragePersistor-experimental` import.
+This utility comes packaged with `react-query` and is available under the `react-query/createAsyncStoragePersister` import.
## Usage
-- Import the `createAsyncStoragePersistor` function
-- Create a new asyncStoragePersistor
+- Import the `createAsyncStoragePersister` function
+- Create a new asyncStoragePersister
- you can pass any `storage` to it that adheres to the `AsyncStorage` interface - the example below uses the async-storage from React Native
- Pass it to the [`persistQueryClient`](./persistQueryClient) function
```ts
import AsyncStorage from '@react-native-async-storage/async-storage'
-import { persistQueryClient } from 'react-query/persistQueryClient-experimental'
-import { createAsyncStoragePersistor } from 'react-query/createAsyncStoragePersistor-experimental'
+import { persistQueryClient } from 'react-query/persistQueryClient'
+import { createAsyncStoragePersister } from 'react-query/createAsyncStoragePersister'
const queryClient = new QueryClient({
defaultOptions: {
@@ -29,30 +27,30 @@ const queryClient = new QueryClient({
},
})
-const asyncStoragePersistor = createAsyncStoragePersistor({
+const asyncStoragePersister = createAsyncStoragePersister({
storage: AsyncStorage
})
persistQueryClient({
queryClient,
- persistor: asyncStoragePersistor,
+ persister: asyncStoragePersister,
})
```
## API
-### `createAsyncStoragePersistor`
+### `createAsyncStoragePersister`
-Call this function to create an asyncStoragePersistor that you can use later with `persistQueryClient`.
+Call this function to create an asyncStoragePersister that you can use later with `persistQueryClient`.
```js
-createAsyncStoragePersistor(options: CreateAsyncStoragePersistorOptions)
+createAsyncStoragePersister(options: CreateAsyncStoragePersisterOptions)
```
### `Options`
```ts
-interface CreateAsyncStoragePersistorOptions {
+interface CreateAsyncStoragePersisterOptions {
/** The storage client used for setting an retrieving items from cache */
storage: AsyncStorage
/** The key to use when storing the cache to localStorage */
diff --git a/docs/src/pages/plugins/createWebStoragePersistor.md b/docs/src/pages/plugins/createWebStoragePersister.md
similarity index 54%
rename from docs/src/pages/plugins/createWebStoragePersistor.md
rename to docs/src/pages/plugins/createWebStoragePersister.md
index 983ac336bf..58f9d93168 100644
--- a/docs/src/pages/plugins/createWebStoragePersistor.md
+++ b/docs/src/pages/plugins/createWebStoragePersister.md
@@ -1,23 +1,21 @@
---
-id: createWebStoragePersistor
-title: createWebStoragePersistor (Experimental)
+id: createWebStoragePersister
+title: createWebStoragePersister
---
-> VERY IMPORTANT: This utility is currently in an experimental stage. This means that breaking changes will happen in minor AND patch releases. Use at your own risk. If you choose to rely on this in production in an experimental stage, please lock your version to a patch-level version to avoid unexpected breakages.
-
## Installation
-This utility comes packaged with `react-query` and is available under the `react-query/createWebStoragePersistor-experimental` import.
+This utility comes packaged with `react-query` and is available under the `react-query/createWebStoragePersister` import.
## Usage
-- Import the `createWebStoragePersistor` function
-- Create a new webStoragePersistor
+- Import the `createWebStoragePersister` function
+- Create a new webStoragePersister
- Pass it to the [`persistQueryClient`](./persistQueryClient) function
```ts
-import { persistQueryClient } from 'react-query/persistQueryClient-experimental'
-import { createWebStoragePersistor } from 'react-query/createWebStoragePersistor-experimental'
+import { persistQueryClient } from 'react-query/persistQueryClient'
+import { createWebStoragePersister } from 'react-query/createWebStoragePersister'
const queryClient = new QueryClient({
defaultOptions: {
@@ -27,29 +25,29 @@ const queryClient = new QueryClient({
},
})
-const localStoragePersistor = createWebStoragePersistor({ storage: window.localStorage })
-// const sessionStoragePersistor = createWebStoragePersistor({ storage: window.sessionStorage })
+const localStoragePersister = createWebStoragePersister({ storage: window.localStorage })
+// const sessionStoragePersister = createWebStoragePersister({ storage: window.sessionStorage })
persistQueryClient({
queryClient,
- persistor: localStoragePersistor,
+ persister: localStoragePersister,
})
```
## API
-### `createWebStoragePersistor`
+### `createWebStoragePersister`
-Call this function to create a webStoragePersistor that you can use later with `persistQueryClient`.
+Call this function to create a webStoragePersister that you can use later with `persistQueryClient`.
```js
-createWebStoragePersistor(options: CreateWebStoragePersistorOptions)
+createWebStoragePersister(options: CreateWebStoragePersisterOptions)
```
### `Options`
```ts
-interface CreateWebStoragePersistorOptions {
+interface CreateWebStoragePersisterOptions {
/** The storage client used for setting an retrieving items from cache (window.localStorage or window.sessionStorage) */
storage: Storage
/** The key to use when storing the cache */
diff --git a/docs/src/pages/plugins/persistQueryClient.md b/docs/src/pages/plugins/persistQueryClient.md
index 1236d07867..969ea98804 100644
--- a/docs/src/pages/plugins/persistQueryClient.md
+++ b/docs/src/pages/plugins/persistQueryClient.md
@@ -1,28 +1,26 @@
---
id: persistQueryClient
-title: persistQueryClient (Experimental)
+title: persistQueryClient
---
-> VERY IMPORTANT: This utility is currently in an experimental stage. This means that breaking changes will happen in minor AND patch releases. Use at your own risk. If you choose to rely on this in production in an experimental stage, please lock your version to a patch-level version to avoid unexpected breakages.
+`persistQueryClient` is a utility for persisting the state of your queryClient and its caches for later use. Different **persisters** can be used to store your client and cache to many different storage layers.
-`persistQueryClient` is a utility for persisting the state of your queryClient and its caches for later use. Different **persistors** can be used to store your client and cache to many different storage layers.
+## Officially Supported Persisters
-## Officially Supported Persistors
-
-- [createWebStoragePersistor (Experimental)](/plugins/createWebStoragePersistor)
-- [createAsyncStoragePersistor (Experimental)](/plugins/createAsyncStoragePersistor)
+- [createWebStoragePersister](/plugins/createWebStoragePersister)
+- [createAsyncStoragePersister](/plugins/createAsyncStoragePersister)
## Installation
-This utility comes packaged with `react-query` and is available under the `react-query/persistQueryClient-experimental` import.
+This utility comes packaged with `react-query` and is available under the `react-query/persistQueryClient` import.
## Usage
-Import the `persistQueryClient` function, and pass it your `QueryClient` instance (with a `cacheTime` set), and a Persistor interface (there are multiple persistor types you can use):
+Import the `persistQueryClient` function, and pass it your `QueryClient` instance (with a `cacheTime` set), and a Persister interface (there are multiple persister types you can use):
```ts
-import { persistQueryClient } from 'react-query/persistQueryClient-experimental'
-import { createWebStoragePersistor } from 'react-query/createWebStoragePersistor-experimental'
+import { persistQueryClient } from 'react-query/persistQueryClient'
+import { createWebStoragePersister } from 'react-query/createWebStoragePersister'
const queryClient = new QueryClient({
defaultOptions: {
@@ -32,11 +30,11 @@ const queryClient = new QueryClient({
},
})
-const localStoragePersistor = createWebStoragePersistor({storage: window.localStorage})
+const localStoragePersister = createWebStoragePersister({storage: window.localStorage})
persistQueryClient({
queryClient,
- persistor: localStoragePersistor,
+ persister: localStoragePersister,
})
```
@@ -52,11 +50,11 @@ You can also pass it `Infinity` to disable garbage collection behavior entirely.
As you use your application:
-- When your query/mutation cache is updated, it will be dehydrated and stored by the persistor you provided. **By default**, this action is throttled to happen at most every 1 second to save on potentially expensive writes to a persistor, but can be customized as you see fit.
+- When your query/mutation cache is updated, it will be dehydrated and stored by the persister you provided. **By default**, this action is throttled to happen at most every 1 second to save on potentially expensive writes to a persister, but can be customized as you see fit.
When you reload/bootstrap your app:
-- Attempts to load a previously persisted dehydrated query/mutation cache from the persistor
+- Attempts to load a previously persisted dehydrated query/mutation cache from the persister
- If a cache is found that is older than the `maxAge` (which by default is 24 hours), it will be discarded. This can be customized as you see fit.
## Cache Busting
@@ -64,17 +62,17 @@ When you reload/bootstrap your app:
Sometimes you may make changes to your application or data that immediately invalidate any and all cached data. If and when this happens, you can pass a `buster` string option to `persistQueryClient`, and if the cache that is found does not also have that buster string, it will be discarded.
```ts
-persistQueryClient({ queryClient, persistor, buster: buildHash })
+persistQueryClient({ queryClient, persister, buster: buildHash })
```
## API
### `persistQueryClient`
-Pass this function a `QueryClient` instance and a persistor that will persist your cache. Both are **required**
+Pass this function a `QueryClient` instance and a persister that will persist your cache. Both are **required**
```ts
-persistQueryClient({ queryClient, persistor })
+persistQueryClient({ queryClient, persister })
```
### `Options`
@@ -85,9 +83,9 @@ An object of options:
interface PersistQueryClientOptions {
/** The QueryClient to persist */
queryClient: QueryClient
- /** The Persistor interface for storing and restoring the cache
+ /** The Persister interface for storing and restoring the cache
* to/from a persisted location */
- persistor: Persistor
+ persister: Persister
/** The max-allowed age of the cache.
* If a persisted cache is found that is older than this
* time, it will be discarded */
@@ -111,12 +109,12 @@ The default options are:
}
```
-## Building a Persistor
+## Building a Persister
-Persistors have the following interface:
+Persisters have the following interface:
```ts
-export interface Persistor {
+export interface Persister {
persistClient(persistClient: PersistedClient): Promisable
restoreClient(): Promisable
removeClient(): Promisable
@@ -133,4 +131,4 @@ export interface PersistedClient {
}
```
-Satisfy all of these interfaces and you've got yourself a persistor!
+Satisfy all of these interfaces and you've got yourself a persister!
diff --git a/docs/src/pages/quick-start.md b/docs/src/pages/quick-start.md
index c55349bd7d..d587a9a98d 100644
--- a/docs/src/pages/quick-start.md
+++ b/docs/src/pages/quick-start.md
@@ -36,13 +36,13 @@ function Todos() {
const queryClient = useQueryClient()
// Queries
- const query = useQuery('todos', getTodos)
+ const query = useQuery(['todos'], getTodos)
// Mutations
const mutation = useMutation(postTodo, {
onSuccess: () => {
// Invalidate and refetch
- queryClient.invalidateQueries('todos')
+ queryClient.invalidateQueries(['todos'])
},
})
diff --git a/docs/src/pages/reference/InfiniteQueryObserver.md b/docs/src/pages/reference/InfiniteQueryObserver.md
index 71d85f5811..45dd93744b 100644
--- a/docs/src/pages/reference/InfiniteQueryObserver.md
+++ b/docs/src/pages/reference/InfiniteQueryObserver.md
@@ -9,7 +9,7 @@ The `InfiniteQueryObserver` can be used to observe and switch between infinite q
```js
const observer = new InfiniteQueryObserver(queryClient, {
- queryKey: 'posts',
+ queryKey: ['posts'],
queryFn: fetchPosts,
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
diff --git a/docs/src/pages/reference/MutationCache.md b/docs/src/pages/reference/MutationCache.md
index 06f1d52e99..404fce8e46 100644
--- a/docs/src/pages/reference/MutationCache.md
+++ b/docs/src/pages/reference/MutationCache.md
@@ -65,8 +65,8 @@ const mutations = mutationCache.getAll()
The `subscribe` method can be used to subscribe to the mutation cache as a whole and be informed of safe/known updates to the cache like mutation states changing or mutations being updated, added or removed.
```js
-const callback = mutation => {
- console.log(mutation)
+const callback = event => {
+ console.log(event.type, event.mutation)
}
const unsubscribe = mutationCache.subscribe(callback)
@@ -74,7 +74,7 @@ const unsubscribe = mutationCache.subscribe(callback)
**Options**
-- `callback: (mutation?: Mutation) => void`
+- `callback: (mutation?: MutationCacheNotifyEvent) => void`
- This function will be called with the mutation cache any time it is updated.
**Returns**
diff --git a/docs/src/pages/reference/QueryClient.md b/docs/src/pages/reference/QueryClient.md
index be3fa19d6a..b195e050d0 100644
--- a/docs/src/pages/reference/QueryClient.md
+++ b/docs/src/pages/reference/QueryClient.md
@@ -18,7 +18,7 @@ const queryClient = new QueryClient({
},
})
-await queryClient.prefetchQuery('posts', fetchPosts)
+await queryClient.prefetchQuery(['posts'], fetchPosts)
```
Its available methods are:
@@ -91,7 +91,7 @@ try {
**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 strictly 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.
+The options for `fetchQuery` are exactly the same as those of [`useQuery`](./useQuery), except the following: `enabled, refetchInterval, refetchIntervalInBackground, refetchOnWindowFocus, refetchOnReconnect, notifyOnChangeProps, onSuccess, onError, onSettled, useErrorBoundary, select, suspense, keepPreviousData, placeholderData`; which are strictly 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**
@@ -205,8 +205,6 @@ This distinction is more a "convenience" for ts devs that know which structure w
`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**.
-After successful changing query's cached data via `setQueryData`, it will also trigger `onSuccess` callback from that query.
-
> 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
@@ -268,14 +266,13 @@ queryClient.setQueriesData(queryKey | filters, updater)
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
+- If you **do not want active queries to refetch**, and simply be marked as invalid, you can use the `refetchType: 'none'` option.
+- If you **want inactive queries to refetch** as well, use the `refetchTye: 'all'` option
```js
-await queryClient.invalidateQueries('posts', {
+await queryClient.invalidateQueries(['posts'], {
exact,
- refetchActive: true,
- refetchInactive: false
+ refetchType: 'active',
}, { throwOnError, cancelRefetch })
```
@@ -283,20 +280,22 @@ await queryClient.invalidateQueries('posts', {
- `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
- `filters?: QueryFilters`: [Query Filters](../guides/filters#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
+ - `refetchType?: 'active' | 'inactive' | 'all' | 'none'`
+ - Defaults to `'active'`
+ - When set to `active`, only queries that match the refetch predicate and are actively being rendered via `useQuery` and friends will be refetched in the background.
+ - When set to `inactive`, only queries that match the refetch predicate and are NOT actively being rendered via `useQuery` and friends will be refetched in the background.
+ - When set to `all`, all queries that match the refetch predicate will be refetched in the background.
+ - When set to `none`, no queries will be refetched, and those that match the refetch predicate will be marked as invalid only.
- `refetchPage: (page: TData, index: number, allPages: TData[]) => boolean`
- Only for [Infinite Queries](../guides/infinite-queries#refetchpage)
- Use this function to specify which pages should be refetched
- `options?: InvalidateOptions`:
- `throwOnError?: boolean`
- When set to `true`, this method will throw if any of the query refetch tasks fail.
- - cancelRefetch?: boolean
- - When set to `true`, then the current request will be cancelled before a new request is made
+ - `cancelRefetch?: boolean`
+ - Defaults to `true`
+ - Per default, a currently running request will be cancelled before a new request is made
+ - When set to `false`, no refetch will be made if there is already a request running.
## `queryClient.refetchQueries`
@@ -312,10 +311,10 @@ await queryClient.refetchQueries()
await queryClient.refetchQueries({ stale: true })
// refetch all active queries partially matching a query key:
-await queryClient.refetchQueries(['posts'], { active: true })
+await queryClient.refetchQueries(['posts'], { type: 'active' })
// refetch all active queries exactly matching a query key:
-await queryClient.refetchQueries(['posts', 1], { active: true, exact: true })
+await queryClient.refetchQueries(['posts', 1], { type: 'active', exact: true })
```
**Options**
@@ -328,8 +327,10 @@ await queryClient.refetchQueries(['posts', 1], { active: true, exact: true })
- `options?: RefetchOptions`:
- `throwOnError?: boolean`
- When set to `true`, this method will throw if any of the query refetch tasks fail.
- - cancelRefetch?: boolean
- - When set to `true`, then the current request will be cancelled before a new request is made
+ - `cancelRefetch?: boolean`
+ - Defaults to `true`
+ - Per default, a currently running request will be cancelled before a new request is made
+ - When set to `false`, no refetch will be made if there is already a request running.
**Returns**
@@ -342,7 +343,7 @@ The `cancelQueries` method can be used to cancel outgoing queries based on their
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 })
+await queryClient.cancelQueries(['posts'], { exact: true })
```
**Options**
@@ -396,8 +397,10 @@ queryClient.resetQueries(queryKey, { exact: true })
- `options?: ResetOptions`:
- `throwOnError?: boolean`
- When set to `true`, this method will throw if any of the query refetch tasks fail.
- - cancelRefetch?: boolean
- - When set to `true`, then the current request will be cancelled before a new request is made
+ - `cancelRefetch?: boolean`
+ - Defaults to `true`
+ - Per default, a currently running request will be cancelled before a new request is made
+ - When set to `false`, no refetch will be made if there is already a request running.
**Returns**
@@ -468,7 +471,7 @@ queryClient.setDefaultOptions({
The `getQueryDefaults` method returns the default options which have been set for specific queries:
```js
-const defaultOptions = queryClient.getQueryDefaults('posts')
+const defaultOptions = queryClient.getQueryDefaults(['posts'])
```
## `queryClient.setQueryDefaults`
@@ -476,10 +479,10 @@ const defaultOptions = queryClient.getQueryDefaults('posts')
`setQueryDefaults` can be used to set default options for specific queries:
```js
-queryClient.setQueryDefaults('posts', { queryFn: fetchPosts })
+queryClient.setQueryDefaults(['posts'], { queryFn: fetchPosts })
function Component() {
- const { data } = useQuery('posts')
+ const { data } = useQuery(['posts'])
}
```
@@ -493,7 +496,7 @@ function Component() {
The `getMutationDefaults` method returns the default options which have been set for specific mutations:
```js
-const defaultOptions = queryClient.getMutationDefaults('addPost')
+const defaultOptions = queryClient.getMutationDefaults(['addPost'])
```
## `queryClient.setMutationDefaults`
@@ -501,10 +504,10 @@ const defaultOptions = queryClient.getMutationDefaults('addPost')
`setMutationDefaults` can be used to set default options for specific mutations:
```js
-queryClient.setMutationDefaults('addPost', { mutationFn: addPost })
+queryClient.setMutationDefaults(['addPost'], { mutationFn: addPost })
function Component() {
- const { data } = useMutation('addPost')
+ const { data } = useMutation(['addPost'])
}
```
diff --git a/docs/src/pages/reference/QueryObserver.md b/docs/src/pages/reference/QueryObserver.md
index 51813453d9..a2f625b905 100644
--- a/docs/src/pages/reference/QueryObserver.md
+++ b/docs/src/pages/reference/QueryObserver.md
@@ -8,7 +8,7 @@ title: QueryObserver
The `QueryObserver` can be used to observe and switch between queries.
```js
-const observer = new QueryObserver(queryClient, { queryKey: 'posts' })
+const observer = new QueryObserver(queryClient, { queryKey: ['posts'] })
const unsubscribe = observer.subscribe(result => {
console.log(result)
diff --git a/docs/src/pages/reference/hydration.md b/docs/src/pages/reference/hydration.md
index 0ea0040063..f1694fb634 100644
--- a/docs/src/pages/reference/hydration.md
+++ b/docs/src/pages/reference/hydration.md
@@ -15,8 +15,6 @@ const dehydratedState = dehydrate(queryClient, {
})
```
-> Note: Since version `3.22.0` hydration utilities moved into to core. If you using lower version your should import `dehydrate` from `react-query/hydration`
-
**Options**
- `client: QueryClient`
@@ -72,8 +70,6 @@ import { hydrate } from 'react-query'
hydrate(queryClient, dehydratedState, options)
```
-> Note: Since version `3.22.0` hydration utilities moved into to core. If you using lower version your should import `hydrate` from `react-query/hydration`
-
**Options**
- `client: QueryClient`
@@ -99,8 +95,6 @@ import { useHydrate } from 'react-query'
useHydrate(dehydratedState, options)
```
-> Note: Since version `3.22.0` hydration utilities moved into to core. If you using lower version your should import `useHydrate` from `react-query/hydration`
-
**Options**
- `dehydratedState: DehydratedState`
@@ -123,8 +117,6 @@ function App() {
}
```
-> Note: Since version `3.22.0` hydration utilities moved into to core. If you using lower version your should import `Hydrate` from `react-query/hydration`
-
**Options**
- `state: DehydratedState`
diff --git a/docs/src/pages/reference/useInfiniteQuery.md b/docs/src/pages/reference/useInfiniteQuery.md
index 8ffaab4255..08d6c9cbf0 100644
--- a/docs/src/pages/reference/useInfiniteQuery.md
+++ b/docs/src/pages/reference/useInfiniteQuery.md
@@ -26,9 +26,7 @@ The options for `useInfiniteQuery` are identical to the [`useQuery` hook](/refer
- `queryFn: (context: QueryFunctionContext) => Promise`
- **Required, but only if no default query function has been defined** [`defaultQueryFn`](/guides/default-query-function)
- The function that the query will use to request data.
- - Receives a `QueryFunctionContext` object with the following variables:
- - `queryKey: EnsuredQueryKey`: the queryKey, guaranteed to be an Array
- - `pageParam: unknown | undefined`
+ - Receives a [QueryFunctionContext](../guides/query-functions#queryfunctioncontext)
- Must return a promise that will either resolve data or throw an error.
- Make sure you return the data *and* the `pageParam` if needed for use in the props below.
- `getNextPageParam: (lastPage, allPages) => unknown | undefined`
diff --git a/docs/src/pages/reference/useMutation.md b/docs/src/pages/reference/useMutation.md
index e8b2e9cdcb..07d82110ca 100644
--- a/docs/src/pages/reference/useMutation.md
+++ b/docs/src/pages/reference/useMutation.md
@@ -17,13 +17,15 @@ const {
reset,
status,
} = useMutation(mutationFn, {
+ cacheTime,
mutationKey,
+ networkMode,
onError,
onMutate,
onSettled,
onSuccess,
useErrorBoundary,
- meta,
+ meta
})
mutate(variables, {
@@ -39,9 +41,16 @@ mutate(variables, {
- **Required**
- A function that performs an asynchronous task and returns a promise.
- `variables` is an object that `mutate` will pass to your `mutationFn`
+- `cacheTime: number | Infinity`
+ - The time in milliseconds that unused/inactive cache data remains in memory. When a mutation's cache becomes unused or inactive, that cache data will be garbage collected after this duration. When different cache times are specified, the longest one will be used.
+ - If set to `Infinity`, will disable garbage collection
- `mutationKey: string`
- Optional
- A mutation key can be set to inherit defaults set with `queryClient.setMutationDefaults` or to identify the mutation in the devtools.
+- `networkMode: 'online' | 'always' | 'offlineFirst`
+ - optional
+ - defaults to `'online'`
+ - see [Network Mode](../guides/network-mode) for more information.
- `onMutate: (variables: TVariables) => Promise | TContext | void`
- Optional
- This function will fire before the mutation function is fired and is passed the same variables the mutation function would receive
@@ -94,6 +103,9 @@ mutate(variables, {
- `error` if the last mutation attempt resulted in an error.
- `success` if the last mutation attempt was successful.
- `isIdle`, `isLoading`, `isSuccess`, `isError`: boolean variables derived from `status`
+- `isPaused: boolean`
+ - will be `true` if the mutation has been `paused`
+ - see [Network Mode](../guides/network-mode) for more information.
- `data: undefined | unknown`
- Defaults to `undefined`
- The last successfully resolved data for the query.
diff --git a/docs/src/pages/reference/useQuery.md b/docs/src/pages/reference/useQuery.md
index 0b5588acc7..972a6fe419 100644
--- a/docs/src/pages/reference/useQuery.md
+++ b/docs/src/pages/reference/useQuery.md
@@ -14,6 +14,7 @@ const {
isFetched,
isFetchedAfterMount,
isFetching,
+ isPaused,
isIdle,
isLoading,
isLoadingError,
@@ -26,16 +27,17 @@ const {
refetch,
remove,
status,
+ fetchStatus,
} = useQuery(queryKey, queryFn?, {
cacheTime,
enabled,
+ networkMode,
initialData,
- initialDataUpdatedAt
+ initialDataUpdatedAt,
isDataEqual,
keepPreviousData,
meta,
notifyOnChangeProps,
- notifyOnChangePropsExclusions,
onError,
onSettled,
onSuccess,
@@ -66,7 +68,7 @@ const result = useQuery({
**Options**
-- `queryKey: string | unknown[]`
+- `queryKey: unknown[]`
- **Required**
- The query key to use for this query.
- The query key will be hashed into a stable hash. See [Query Keys](../guides/query-keys) for more information.
@@ -74,12 +76,15 @@ const result = useQuery({
- `queryFn: (context: QueryFunctionContext) => Promise`
- **Required, but only if no default query function has been defined** See [Default Query Function](../guides/default-query-function) for more information.
- The function that the query will use to request data.
- - Receives a `QueryFunctionContext` object with the following variables:
- - `queryKey: EnsuredQueryKey`: the queryKey, guaranteed to be an Array
+ - Receives a [QueryFunctionContext](../guides/query-functions#queryfunctioncontext)
- Must return a promise that will either resolve data or throw an error.
- `enabled: boolean`
- Set this to `false` to disable this query from automatically running.
- Can be used for [Dependent Queries](../guides/dependent-queries).
+- `networkMode: 'online' | 'always' | 'offlineFirst`
+ - optional
+ - defaults to `'online'`
+ - see [Network Mode](../guides/network-mode) for more information.
- `retry: boolean | number | (failureCount: number, error: TError) => boolean`
- If `false`, failed queries will not retry by default.
- If `true`, failed queries will retry infinitely.
@@ -126,18 +131,15 @@ const result = useQuery({
- If set to `true`, the query will refetch on reconnect if the data is stale.
- If set to `false`, the query will not refetch on reconnect.
- If set to `"always"`, the query will always refetch on reconnect.
-- `notifyOnChangeProps: string[] | "tracked"`
+- `notifyOnChangeProps: string[] | "all"`
- Optional
- If set, the component will only re-render if any of the listed properties change.
- If set to `['data', 'error']` for example, the component will only re-render when the `data` or `error` properties change.
- - If set to `"tracked"`, access to properties will be tracked, and the component will only re-render when one of the tracked properties change.
-- `notifyOnChangePropsExclusions: string[]`
- - Optional
- - If set, the component will not re-render if any of the listed properties change.
- - If set to `['isStale']` for example, the component will not re-render when the `isStale` property changes.
+ - If set to `"all"`, the component will opt-out of smart tracking and re-render whenever a query is updated.
+ - By default, access to properties will be tracked, and the component will only re-render when one of the tracked properties change.
- `onSuccess: (data: TData) => void`
- Optional
- - This function will fire any time the query successfully fetches new data or the cache is updated via `setQueryData`.
+ - This function will fire any time the query successfully fetches new data.
- `onError: (error: TError) => void`
- Optional
- This function will fire if the query encounters an error and will be passed the error.
@@ -223,9 +225,15 @@ const result = useQuery({
- `isFetchedAfterMount: boolean`
- 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.
+- `fetchStatus: FetchStatus`
+ - `fetching`: Is `true` whenever the queryFn is executing, which includes initial `loading` as well as background refetches.
+ - `paused`: The query wanted to fetch, but has been `paused`
+ - `idle`: The query is not fetching
+ - see [Network Mode](../guides/network-mode) for more information.
- `isFetching: boolean`
- - 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.
+ - A derived boolean from the `fetchStatus` variable above, provided for convenience.
+- `isPaused: boolean`
+ - A derived boolean from the `fetchStatus` variable above, provided for convenience.
- `isRefetching: boolean`
- Is `true` whenever a background refetch is in-flight, which _does not_ include initial `loading`
- Is the same as `isFetching && !isLoading`
@@ -236,6 +244,9 @@ const result = useQuery({
- `refetch: (options: { throwOnError: boolean, cancelRefetch: boolean }) => Promise`
- A function to manually refetch the query.
- If the query errors, the error will only be logged. If you want an error to be thrown, pass the `throwOnError: true` option
- - If `cancelRefetch` is `true`, then the current request will be cancelled before a new request is made
+ - `cancelRefetch?: boolean`
+ - Defaults to `true`
+ - Per default, a currently running request will be cancelled before a new request is made
+ - When set to `false`, no refetch will be made if there is already a request running.
- `remove: () => void`
- A function to remove the query from the cache.
diff --git a/examples/basic-typescript/src/index.tsx b/examples/basic-typescript/src/index.tsx
index 1b4c8e9b29..4a969879e7 100644
--- a/examples/basic-typescript/src/index.tsx
+++ b/examples/basic-typescript/src/index.tsx
@@ -20,7 +20,7 @@ type Post = {
function usePosts() {
return useQuery(
- "posts",
+ ["posts"],
async (): Promise> => {
const { data } = await axios.get(
"https://jsonplaceholder.typicode.com/posts"
diff --git a/examples/basic/src/index.js b/examples/basic/src/index.js
index 722e2bcc7c..638eef5425 100644
--- a/examples/basic/src/index.js
+++ b/examples/basic/src/index.js
@@ -9,8 +9,23 @@ import {
QueryClientProvider,
} from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
+import { persistQueryClient } from 'react-query/persistQueryClient';
+import { createWebStoragePersister } from 'react-query/createWebStoragePersister'
-const queryClient = new QueryClient();
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ cacheTime: 1000 * 60 * 60 * 24, // 24 hours
+ },
+ },
+})
+
+const localStoragePersister = createWebStoragePersister({storage: window.localStorage})
+
+persistQueryClient({
+ queryClient,
+ persister: localStoragePersister,
+})
function App() {
const [postId, setPostId] = React.useState(-1);
@@ -38,7 +53,7 @@ function App() {
}
function usePosts() {
- return useQuery("posts", async () => {
+ return useQuery(["posts"], async () => {
const { data } = await axios.get(
"https://jsonplaceholder.typicode.com/posts"
);
diff --git a/hydration/package.json b/hydration/package.json
deleted file mode 100644
index 804509cb4d..0000000000
--- a/hydration/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "internal": true,
- "main": "../lib/hydration/index.js",
- "module": "../es/hydration/index.js",
- "types": "../types/hydration/index.d.ts"
-}
diff --git a/package.json b/package.json
index 560a5dcec6..0c927899a0 100644
--- a/package.json
+++ b/package.json
@@ -16,13 +16,13 @@
"module": "es/index.js",
"sideEffects": [
"es/index.js",
- "es/react/index.js",
- "es/react/setBatchUpdatesFn.js",
- "es/react/setLogger.js",
+ "es/reactjs/index.js",
+ "es/reactjs/setBatchUpdatesFn.js",
+ "es/reactjs/setLogger.js",
"lib/index.js",
- "lib/react/index.js",
- "lib/react/setBatchUpdatesFn.js",
- "lib/react/setLogger.js"
+ "lib/reactjs/index.js",
+ "lib/reactjs/setBatchUpdatesFn.js",
+ "lib/reactjs/setLogger.js"
],
"scripts": {
"test": "is-ci \"test:ci\" \"test:dev\"",
@@ -55,12 +55,12 @@
"es",
"hydration",
"devtools",
- "persistQueryClient-experimental",
- "createWebStoragePersistor-experimental",
- "createAsyncStoragePersistor-experimental",
+ "persistQueryClient",
+ "createWebStoragePersister",
+ "createAsyncStoragePersister",
"broadcastQueryClient-experimental",
"lib",
- "react",
+ "reactjs",
"scripts",
"types"
],
@@ -81,7 +81,11 @@
}
},
"typesVersions": {
- "<4.1": { "types/*": ["types/ts3.8/*"] }
+ "<4.1": {
+ "types/*": [
+ "types/ts3.8/*"
+ ]
+ }
},
"devDependencies": {
"@babel/cli": "^7.11.6",
diff --git a/persistQueryClient-experimental/package.json b/persistQueryClient-experimental/package.json
deleted file mode 100644
index f58433722d..0000000000
--- a/persistQueryClient-experimental/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "internal": true,
- "main": "../lib/persistQueryClient-experimental/index.js",
- "module": "../es/persistQueryClient-experimental/index.js",
- "types": "../types/persistQueryClient-experimental/index.d.ts"
-}
diff --git a/persistQueryClient/package.json b/persistQueryClient/package.json
new file mode 100644
index 0000000000..576ea12afb
--- /dev/null
+++ b/persistQueryClient/package.json
@@ -0,0 +1,6 @@
+{
+ "internal": true,
+ "main": "../lib/persistQueryClient/index.js",
+ "module": "../es/persistQueryClient/index.js",
+ "types": "../types/persistQueryClient/index.d.ts"
+}
diff --git a/react/package.json b/react/package.json
deleted file mode 100644
index 02f74bc64a..0000000000
--- a/react/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "internal": true,
- "main": "../lib/react/index.js",
- "module": "../es/react/index.js",
- "types": "../types/react/index.d.ts"
-}
diff --git a/reactjs/package.json b/reactjs/package.json
new file mode 100644
index 0000000000..8d0e431ae3
--- /dev/null
+++ b/reactjs/package.json
@@ -0,0 +1,6 @@
+{
+ "internal": true,
+ "main": "../lib/reactjs/index.js",
+ "module": "../es/reactjs/index.js",
+ "types": "../types/reactjs/index.d.ts"
+}
diff --git a/rollup.config.js b/rollup.config.js
index 528bebbaf5..35fdd215aa 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -19,21 +19,20 @@ const inputSrcs = [
['src/index.ts', 'ReactQuery', 'react-query'],
['src/core/index.ts', 'ReactQueryCore', 'react-query-core'],
['src/devtools/index.ts', 'ReactQueryDevtools', 'react-query-devtools'],
- ['src/hydration/index.ts', 'ReactQueryHydration', 'react-query-hydration'],
[
- 'src/persistQueryClient-experimental/index.ts',
- 'ReactQueryPersistQueryClientExperimental',
- 'persistQueryClient-experimental',
+ 'src/persistQueryClient/index.ts',
+ 'ReactQueryPersistQueryClient',
+ 'persistQueryClient',
],
[
- 'src/createWebStoragePersistor-experimental/index.ts',
- 'ReactQueryCreateWebStoragePersistorExperimental',
- 'createWebStoragePersistor-experimental',
+ 'src/createWebStoragePersister/index.ts',
+ 'ReactQueryCreateWebStoragePersister',
+ 'createWebStoragePersister',
],
[
- 'src/createAsyncStoragePersistor-experimental/index.ts',
- 'ReactQueryCreateAsyncStoragePersistorExperimental',
- 'createAsyncStoragePersistor-experimental',
+ 'src/createAsyncStoragePersister/index.ts',
+ 'ReactQueryCreateAsyncStoragePersister',
+ 'createAsyncStoragePersister',
],
[
'src/broadcastQueryClient-experimental/index.ts',
diff --git a/src/broadcastQueryClient-experimental/index.ts b/src/broadcastQueryClient-experimental/index.ts
index 9f6181553a..6a36d1c339 100644
--- a/src/broadcastQueryClient-experimental/index.ts
+++ b/src/broadcastQueryClient-experimental/index.ts
@@ -33,20 +33,20 @@ export function broadcastQueryClient({
} = queryEvent
if (
- queryEvent.type === 'queryUpdated' &&
+ queryEvent.type === 'updated' &&
queryEvent.action?.type === 'success'
) {
channel.postMessage({
- type: 'queryUpdated',
+ type: 'updated',
queryHash,
queryKey,
state,
})
}
- if (queryEvent.type === 'queryRemoved') {
+ if (queryEvent.type === 'removed') {
channel.postMessage({
- type: 'queryRemoved',
+ type: 'removed',
queryHash,
queryKey,
})
@@ -61,7 +61,7 @@ export function broadcastQueryClient({
tx(() => {
const { type, queryHash, queryKey, state } = action
- if (type === 'queryUpdated') {
+ if (type === 'updated') {
const query = queryCache.get(queryHash)
if (query) {
@@ -77,7 +77,7 @@ export function broadcastQueryClient({
},
state
)
- } else if (type === 'queryRemoved') {
+ } else if (type === 'removed') {
const query = queryCache.get(queryHash)
if (query) {
diff --git a/src/core/infiniteQueryBehavior.ts b/src/core/infiniteQueryBehavior.ts
index 0bd46be8e0..3fe93a0377 100644
--- a/src/core/infiniteQueryBehavior.ts
+++ b/src/core/infiniteQueryBehavior.ts
@@ -1,5 +1,5 @@
import type { QueryBehavior } from './query'
-import { isCancelable } from './retryer'
+
import type {
InfiniteData,
QueryFunctionContext,
@@ -73,11 +73,6 @@ export function infiniteQueryBehavior<
buildNewPages(pages, param, page, previous)
)
- if (isCancelable(queryFnResult)) {
- const promiseAsAny = promise as any
- promiseAsAny.cancel = queryFnResult.cancel
- }
-
return promise
}
@@ -148,15 +143,10 @@ export function infiniteQueryBehavior<
pageParams: newPageParams,
}))
- const finalPromiseAsAny = finalPromise as any
-
- finalPromiseAsAny.cancel = () => {
+ context.signal?.addEventListener('abort', () => {
cancelled = true
abortController?.abort()
- if (isCancelable(promise)) {
- promise.cancel()
- }
- }
+ })
return finalPromise
}
diff --git a/src/core/infiniteQueryObserver.ts b/src/core/infiniteQueryObserver.ts
index 19ef264650..dc0a6a6021 100644
--- a/src/core/infiniteQueryObserver.ts
+++ b/src/core/infiniteQueryObserver.ts
@@ -39,7 +39,7 @@ export class InfiniteQueryObserver<
// Type override
protected fetch!: (
- fetchOptions?: ObserverFetchOptions
+ fetchOptions: ObserverFetchOptions
) => Promise>
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
@@ -90,28 +90,27 @@ export class InfiniteQueryObserver<
>
}
- fetchNextPage(
- options?: FetchNextPageOptions
- ): Promise> {
+ fetchNextPage({ pageParam, ...options }: FetchNextPageOptions = {}): Promise<
+ InfiniteQueryObserverResult
+ > {
return this.fetch({
- // TODO consider removing `?? true` in future breaking change, to be consistent with `refetch` API (see https://github.com/tannerlinsley/react-query/issues/2617)
- cancelRefetch: options?.cancelRefetch ?? true,
- throwOnError: options?.throwOnError,
+ ...options,
meta: {
- fetchMore: { direction: 'forward', pageParam: options?.pageParam },
+ fetchMore: { direction: 'forward', pageParam },
},
})
}
- fetchPreviousPage(
- options?: FetchPreviousPageOptions
- ): Promise> {
+ fetchPreviousPage({
+ pageParam,
+ ...options
+ }: FetchPreviousPageOptions = {}): Promise<
+ InfiniteQueryObserverResult
+ > {
return this.fetch({
- // TODO consider removing `?? true` in future breaking change, to be consistent with `refetch` API (see https://github.com/tannerlinsley/react-query/issues/2617)
- cancelRefetch: options?.cancelRefetch ?? true,
- throwOnError: options?.throwOnError,
+ ...options,
meta: {
- fetchMore: { direction: 'backward', pageParam: options?.pageParam },
+ fetchMore: { direction: 'backward', pageParam },
},
})
}
@@ -134,9 +133,10 @@ export class InfiniteQueryObserver<
hasNextPage: hasNextPage(options, state.data?.pages),
hasPreviousPage: hasPreviousPage(options, state.data?.pages),
isFetchingNextPage:
- state.isFetching && state.fetchMeta?.fetchMore?.direction === 'forward',
+ state.fetchStatus === 'fetching' &&
+ state.fetchMeta?.fetchMore?.direction === 'forward',
isFetchingPreviousPage:
- state.isFetching &&
+ state.fetchStatus === 'fetching' &&
state.fetchMeta?.fetchMore?.direction === 'backward',
}
}
diff --git a/src/core/mutation.ts b/src/core/mutation.ts
index 58796a3a6b..093f4107ee 100644
--- a/src/core/mutation.ts
+++ b/src/core/mutation.ts
@@ -3,7 +3,8 @@ import type { MutationCache } from './mutationCache'
import type { MutationObserver } from './mutationObserver'
import { getLogger } from './logger'
import { notifyManager } from './notifyManager'
-import { Retryer } from './retryer'
+import { Removable } from './removable'
+import { canFetch, Retryer } from './retryer'
import { noop } from './utils'
// TYPES
@@ -81,7 +82,7 @@ export class Mutation<
TError = unknown,
TVariables = void,
TContext = unknown
-> {
+> extends Removable {
state: MutationState
options: MutationOptions
mutationId: number
@@ -92,6 +93,8 @@ export class Mutation<
private retryer?: Retryer
constructor(config: MutationConfig) {
+ super()
+
this.options = {
...config.defaultOptions,
...config.options,
@@ -101,6 +104,9 @@ export class Mutation<
this.observers = []
this.state = config.state || getDefaultState()
this.meta = config.meta
+
+ this.updateCacheTime(this.options.cacheTime)
+ this.scheduleGc()
}
setState(state: MutationState): void {
@@ -110,11 +116,38 @@ export class Mutation<
addObserver(observer: MutationObserver): void {
if (this.observers.indexOf(observer) === -1) {
this.observers.push(observer)
+
+ // Stop the mutation from being garbage collected
+ this.clearGcTimeout()
+
+ this.mutationCache.notify({
+ type: 'observerAdded',
+ mutation: this,
+ observer,
+ })
}
}
removeObserver(observer: MutationObserver): void {
this.observers = this.observers.filter(x => x !== observer)
+
+ this.scheduleGc()
+
+ this.mutationCache.notify({
+ type: 'observerRemoved',
+ mutation: this,
+ observer,
+ })
+ }
+
+ protected optionalRemove() {
+ if (!this.observers.length) {
+ if (this.state.status === 'loading') {
+ this.scheduleGc()
+ } else {
+ this.mutationCache.remove(this)
+ }
+ }
}
cancel(): Promise {
@@ -247,21 +280,82 @@ export class Mutation<
},
retry: this.options.retry ?? 0,
retryDelay: this.options.retryDelay,
+ networkMode: this.options.networkMode,
})
return this.retryer.promise
}
private dispatch(action: Action): void {
- this.state = reducer(this.state, action)
+ this.state = this.reducer(action)
notifyManager.batch(() => {
this.observers.forEach(observer => {
observer.onMutationUpdate(action)
})
- this.mutationCache.notify(this)
+ this.mutationCache.notify({
+ mutation: this,
+ type: 'updated',
+ action,
+ })
})
}
+
+ private reducer(
+ action: Action
+ ): MutationState {
+ switch (action.type) {
+ case 'failed':
+ return {
+ ...this.state,
+ failureCount: this.state.failureCount + 1,
+ }
+ case 'pause':
+ return {
+ ...this.state,
+ isPaused: true,
+ }
+ case 'continue':
+ return {
+ ...this.state,
+ isPaused: false,
+ }
+ case 'loading':
+ return {
+ ...this.state,
+ context: action.context,
+ data: undefined,
+ error: null,
+ isPaused: !canFetch(this.options.networkMode),
+ status: 'loading',
+ variables: action.variables,
+ }
+ case 'success':
+ return {
+ ...this.state,
+ data: action.data,
+ error: null,
+ status: 'success',
+ isPaused: false,
+ }
+ case 'error':
+ return {
+ ...this.state,
+ data: undefined,
+ error: action.error,
+ failureCount: this.state.failureCount + 1,
+ isPaused: false,
+ status: 'error',
+ }
+ case 'setState':
+ return {
+ ...this.state,
+ ...action.state,
+ }
+ default:
+ return this.state
+ }
+ }
}
export function getDefaultState<
@@ -280,60 +374,3 @@ export function getDefaultState<
variables: undefined,
}
}
-
-function reducer(
- state: MutationState,
- action: Action
-): MutationState {
- switch (action.type) {
- case 'failed':
- return {
- ...state,
- failureCount: state.failureCount + 1,
- }
- case 'pause':
- return {
- ...state,
- isPaused: true,
- }
- case 'continue':
- return {
- ...state,
- isPaused: false,
- }
- case 'loading':
- return {
- ...state,
- context: action.context,
- data: undefined,
- error: null,
- isPaused: false,
- status: 'loading',
- variables: action.variables,
- }
- case 'success':
- return {
- ...state,
- data: action.data,
- error: null,
- status: 'success',
- isPaused: false,
- }
- case 'error':
- return {
- ...state,
- data: undefined,
- error: action.error,
- failureCount: state.failureCount + 1,
- isPaused: false,
- status: 'error',
- }
- case 'setState':
- return {
- ...state,
- ...action.state,
- }
- default:
- return state
- }
-}
diff --git a/src/core/mutationCache.ts b/src/core/mutationCache.ts
index 624e6972a2..d333ccb26f 100644
--- a/src/core/mutationCache.ts
+++ b/src/core/mutationCache.ts
@@ -1,9 +1,10 @@
+import { MutationObserver } from './mutationObserver'
import type { MutationOptions } from './types'
import type { QueryClient } from './queryClient'
import { notifyManager } from './notifyManager'
-import { Mutation, MutationState } from './mutation'
+import { Action, Mutation, MutationState } from './mutation'
import { matchMutation, MutationFilters, noop } from './utils'
-import { Subscribable } from './subscribable'
+import { Notifiable } from './notifiable'
// TYPES
@@ -12,13 +13,13 @@ interface MutationCacheConfig {
error: unknown,
variables: unknown,
context: unknown,
- mutation: Mutation
+ mutation: Mutation
) => void
onSuccess?: (
data: unknown,
variables: unknown,
context: unknown,
- mutation: Mutation
+ mutation: Mutation
) => void
onMutate?: (
variables: unknown,
@@ -26,11 +27,43 @@ interface MutationCacheConfig {
) => void
}
-type MutationCacheListener = (mutation?: Mutation) => void
+interface NotifyEventMutationAdded {
+ type: 'added'
+ mutation: Mutation
+}
+interface NotifyEventMutationRemoved {
+ type: 'removed'
+ mutation: Mutation
+}
+
+interface NotifyEventMutationObserverAdded {
+ type: 'observerAdded'
+ mutation: Mutation
+ observer: MutationObserver
+}
+
+interface NotifyEventMutationObserverRemoved {
+ type: 'observerRemoved'
+ mutation: Mutation
+ observer: MutationObserver
+}
+
+interface NotifyEventMutationUpdated {
+ type: 'updated'
+ mutation: Mutation
+ action: Action
+}
+
+type MutationCacheNotifyEvent =
+ | NotifyEventMutationAdded
+ | NotifyEventMutationRemoved
+ | NotifyEventMutationObserverAdded
+ | NotifyEventMutationObserverRemoved
+ | NotifyEventMutationUpdated
// CLASS
-export class MutationCache extends Subscribable {
+export class MutationCache extends Notifiable {
config: MutationCacheConfig
private mutations: Mutation[]
@@ -66,13 +99,13 @@ export class MutationCache extends Subscribable {
add(mutation: Mutation): void {
this.mutations.push(mutation)
- this.notify(mutation)
+ this.notify({ type: 'added', mutation })
}
remove(mutation: Mutation): void {
this.mutations = this.mutations.filter(x => x !== mutation)
mutation.cancel()
- this.notify(mutation)
+ this.notify({ type: 'removed', mutation })
}
clear(): void {
@@ -101,14 +134,6 @@ export class MutationCache extends Subscribable {
return this.mutations.filter(mutation => matchMutation(filters, mutation))
}
- notify(mutation?: Mutation) {
- notifyManager.batch(() => {
- this.listeners.forEach(listener => {
- listener(mutation)
- })
- })
- }
-
onFocus(): void {
this.resumePausedMutations()
}
diff --git a/src/core/notifiable.ts b/src/core/notifiable.ts
new file mode 100644
index 0000000000..3d2bcd857f
--- /dev/null
+++ b/src/core/notifiable.ts
@@ -0,0 +1,12 @@
+import { Subscribable } from './subscribable'
+import { notifyManager } from '../core/notifyManager'
+
+export class Notifiable extends Subscribable<(event: TEvent) => void> {
+ notify(event: TEvent) {
+ notifyManager.batch(() => {
+ this.listeners.forEach(listener => {
+ listener(event)
+ })
+ })
+ }
+}
diff --git a/src/core/queriesObserver.ts b/src/core/queriesObserver.ts
index 2eb22ef8b5..79d1bbc168 100644
--- a/src/core/queriesObserver.ts
+++ b/src/core/queriesObserver.ts
@@ -1,6 +1,10 @@
import { difference, replaceAt } from './utils'
import { notifyManager } from './notifyManager'
-import type { QueryObserverOptions, QueryObserverResult } from './types'
+import type {
+ QueryObserverOptions,
+ QueryObserverResult,
+ DefaultedQueryObserverOptions,
+} from './types'
import type { QueryClient } from './queryClient'
import { NotifyOptions, QueryObserver } from './queryObserver'
import { Subscribable } from './subscribable'
@@ -74,7 +78,7 @@ export class QueriesObserver extends Subscribable {
): QueryObserverMatch[] {
const prevObservers = this.observers
const defaultedQueryOptions = queries.map(options =>
- this.client.defaultQueryObserverOptions(options)
+ this.client.defaultQueryOptions(options)
)
const matchingObservers: QueryObserverMatch[] = defaultedQueryOptions.flatMap(
@@ -134,7 +138,7 @@ export class QueriesObserver extends Subscribable {
}
private getObserver(options: QueryObserverOptions): QueryObserver {
- const defaultedOptions = this.client.defaultQueryObserverOptions(options)
+ const defaultedOptions = this.client.defaultQueryOptions(options)
const currentObserver = this.observersMap[defaultedOptions.queryHash!]
return currentObserver ?? new QueryObserver(this.client, defaultedOptions)
}
@@ -205,6 +209,6 @@ export class QueriesObserver extends Subscribable {
}
type QueryObserverMatch = {
- defaultedQueryOptions: QueryObserverOptions
+ defaultedQueryOptions: DefaultedQueryObserverOptions
observer: QueryObserver
}
diff --git a/src/core/query.ts b/src/core/query.ts
index 1b5a3854ad..12e99622ed 100644
--- a/src/core/query.ts
+++ b/src/core/query.ts
@@ -2,11 +2,9 @@ import {
getAbortController,
Updater,
functionalUpdate,
- isValidTimeout,
noop,
replaceEqualDeep,
timeUntilStale,
- ensureQueryKeyArray,
} from './utils'
import type {
InitialDataFunction,
@@ -14,16 +12,17 @@ import type {
QueryOptions,
QueryStatus,
QueryFunctionContext,
- EnsuredQueryKey,
QueryMeta,
CancelOptions,
SetDataOptions,
+ FetchStatus,
} from './types'
import type { QueryCache } from './queryCache'
import type { QueryObserver } from './queryObserver'
import { notifyManager } from './notifyManager'
import { getLogger } from './logger'
-import { Retryer, isCancelledError } from './retryer'
+import { Retryer, isCancelledError, canFetch } from './retryer'
+import { Removable } from './removable'
// TYPES
@@ -51,10 +50,9 @@ export interface QueryState {
errorUpdatedAt: number
fetchFailureCount: number
fetchMeta: any
- isFetching: boolean
isInvalidated: boolean
- isPaused: boolean
status: QueryStatus
+ fetchStatus: FetchStatus
}
export interface FetchContext<
@@ -65,8 +63,9 @@ export interface FetchContext<
> {
fetchFn: () => unknown | Promise
fetchOptions?: FetchOptions
+ signal?: AbortSignal
options: QueryOptions
- queryKey: EnsuredQueryKey
+ queryKey: TQueryKey
state: QueryState
meta: QueryMeta | undefined
}
@@ -100,6 +99,7 @@ interface SuccessAction {
data: TData | undefined
type: 'success'
dataUpdatedAt?: number
+ notifySuccess?: boolean
}
interface ErrorAction {
@@ -146,28 +146,27 @@ export class Query<
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey
-> {
+> extends Removable {
queryKey: TQueryKey
queryHash: string
options!: QueryOptions
initialState: QueryState
revertState?: QueryState
state: QueryState
- cacheTime!: number
meta: QueryMeta | undefined
+ isFetchingOptimistic?: boolean
private cache: QueryCache
private promise?: Promise
- private gcTimeout?: number
private retryer?: Retryer
private observers: QueryObserver[]
private defaultOptions?: QueryOptions
private abortSignalConsumed: boolean
- private hadObservers: boolean
constructor(config: QueryConfig) {
+ super()
+
this.abortSignalConsumed = false
- this.hadObservers = false
this.defaultOptions = config.defaultOptions
this.setOptions(config.options)
this.observers = []
@@ -177,7 +176,6 @@ export class Query<
this.initialState = config.state || this.getDefaultState(this.options)
this.state = this.initialState
this.meta = config.meta
- this.scheduleGc()
}
private setOptions(
@@ -187,11 +185,7 @@ export class Query<
this.meta = options?.meta
- // Default to 5 minutes if not cache time is set
- this.cacheTime = Math.max(
- this.cacheTime || 0,
- this.options.cacheTime ?? 5 * 60 * 1000
- )
+ this.updateCacheTime(this.options.cacheTime)
}
setDefaultOptions(
@@ -200,36 +194,15 @@ export class Query<
this.defaultOptions = options
}
- private scheduleGc(): void {
- this.clearGcTimeout()
-
- if (isValidTimeout(this.cacheTime)) {
- this.gcTimeout = setTimeout(() => {
- this.optionalRemove()
- }, this.cacheTime)
- }
- }
-
- private clearGcTimeout() {
- clearTimeout(this.gcTimeout)
- this.gcTimeout = undefined
- }
-
- private optionalRemove() {
- if (!this.observers.length) {
- if (this.state.isFetching) {
- if (this.hadObservers) {
- this.scheduleGc()
- }
- } else {
- this.cache.remove(this)
- }
+ protected optionalRemove() {
+ if (!this.observers.length && this.state.fetchStatus === 'idle') {
+ this.cache.remove(this)
}
}
setData(
updater: Updater,
- options?: SetDataOptions
+ options?: SetDataOptions & { notifySuccess: boolean }
): TData {
const prevData = this.state.data
@@ -249,6 +222,7 @@ export class Query<
data,
type: 'success',
dataUpdatedAt: options?.updatedAt,
+ notifySuccess: options?.notifySuccess,
})
return data
@@ -268,7 +242,8 @@ export class Query<
}
destroy(): void {
- this.clearGcTimeout()
+ super.destroy()
+
this.cancel({ silent: true })
}
@@ -281,10 +256,6 @@ export class Query<
return this.observers.some(observer => observer.options.enabled !== false)
}
- isFetching(): boolean {
- return this.state.isFetching
- }
-
isStale(): boolean {
return (
this.state.isInvalidated ||
@@ -305,7 +276,7 @@ export class Query<
const observer = this.observers.find(x => x.shouldFetchOnWindowFocus())
if (observer) {
- observer.refetch()
+ observer.refetch({ cancelRefetch: false })
}
// Continue fetch if currently paused
@@ -316,7 +287,7 @@ export class Query<
const observer = this.observers.find(x => x.shouldFetchOnReconnect())
if (observer) {
- observer.refetch()
+ observer.refetch({ cancelRefetch: false })
}
// Continue fetch if currently paused
@@ -326,7 +297,6 @@ export class Query<
addObserver(observer: QueryObserver): void {
if (this.observers.indexOf(observer) === -1) {
this.observers.push(observer)
- this.hadObservers = true
// Stop the query from being garbage collected
this.clearGcTimeout()
@@ -343,18 +313,14 @@ export class Query<
// If the transport layer does not support cancellation
// we'll let the query continue so the result can be cached
if (this.retryer) {
- if (this.retryer.isTransportCancelable || this.abortSignalConsumed) {
+ if (this.abortSignalConsumed) {
this.retryer.cancel({ revert: true })
} else {
this.retryer.cancelRetry()
}
}
- if (this.cacheTime) {
- this.scheduleGc()
- } else {
- this.cache.remove(this)
- }
+ this.scheduleGc()
}
this.cache.notify({ type: 'observerRemoved', query: this, observer })
@@ -375,7 +341,7 @@ export class Query<
options?: QueryOptions,
fetchOptions?: FetchOptions
): Promise {
- if (this.state.isFetching) {
+ if (this.state.fetchStatus !== 'idle') {
if (this.state.dataUpdatedAt && fetchOptions?.cancelRefetch) {
// Silently cancel current fetch if the user wants to cancel refetches
this.cancel({ silent: true })
@@ -401,26 +367,32 @@ export class Query<
}
}
- const queryKey = ensureQueryKeyArray(this.queryKey)
const abortController = getAbortController()
// Create query function context
const queryFnContext: QueryFunctionContext = {
- queryKey,
+ queryKey: this.queryKey,
pageParam: undefined,
meta: this.meta,
}
- Object.defineProperty(queryFnContext, 'signal', {
- enumerable: true,
- get: () => {
- if (abortController) {
- this.abortSignalConsumed = true
- return abortController.signal
- }
- return undefined
- },
- })
+ // Adds an enumerable signal property to the object that
+ // which sets abortSignalConsumed to true when the signal
+ // is read.
+ const addSignalProperty = (object: unknown) => {
+ Object.defineProperty(object, 'signal', {
+ enumerable: true,
+ get: () => {
+ if (abortController) {
+ this.abortSignalConsumed = true
+ return abortController.signal
+ }
+ return undefined
+ },
+ })
+ }
+
+ addSignalProperty(queryFnContext)
// Create fetch function
const fetchFn = () => {
@@ -435,12 +407,14 @@ export class Query<
const context: FetchContext = {
fetchOptions,
options: this.options,
- queryKey: queryKey,
+ queryKey: this.queryKey,
state: this.state,
fetchFn,
meta: this.meta,
}
+ addSignalProperty(context)
+
if (this.options.behavior?.onFetch) {
this.options.behavior?.onFetch(context)
}
@@ -450,7 +424,7 @@ export class Query<
// Set to fetching state if not already in it
if (
- !this.state.isFetching ||
+ this.state.fetchStatus === 'idle' ||
this.state.fetchMeta !== context.fetchOptions?.meta
) {
this.dispatch({ type: 'fetch', meta: context.fetchOptions?.meta })
@@ -466,10 +440,11 @@ export class Query<
// Notify cache callback
this.cache.config.onSuccess?.(data, this as Query)
- // Remove query after fetching if cache time is 0
- if (this.cacheTime === 0) {
- this.optionalRemove()
+ if (!this.isFetchingOptimistic) {
+ // Schedule query gc after fetching
+ this.scheduleGc()
}
+ this.isFetchingOptimistic = false
},
onError: (error: TError | { silent?: boolean }) => {
// Optimistically update state if needed
@@ -488,10 +463,11 @@ export class Query<
getLogger().error(error)
}
- // Remove query after fetching if cache time is 0
- if (this.cacheTime === 0) {
- this.optionalRemove()
+ if (!this.isFetchingOptimistic) {
+ // Schedule query gc after fetching
+ this.scheduleGc()
}
+ this.isFetchingOptimistic = false
},
onFail: () => {
this.dispatch({ type: 'failed' })
@@ -504,6 +480,7 @@ export class Query<
},
retry: context.options.retry,
retryDelay: context.options.retryDelay,
+ networkMode: context.options.networkMode,
})
this.promise = this.retryer.promise
@@ -519,7 +496,7 @@ export class Query<
observer.onQueryUpdate(action)
})
- this.cache.notify({ query: this, type: 'queryUpdated', action })
+ this.cache.notify({ query: this, type: 'updated', action })
})
}
@@ -550,10 +527,9 @@ export class Query<
errorUpdatedAt: 0,
fetchFailureCount: 0,
fetchMeta: null,
- isFetching: false,
isInvalidated: false,
- isPaused: false,
status: hasData ? 'success' : 'idle',
+ fetchStatus: 'idle',
}
}
@@ -570,20 +546,21 @@ export class Query<
case 'pause':
return {
...state,
- isPaused: true,
+ fetchStatus: 'paused',
}
case 'continue':
return {
...state,
- isPaused: false,
+ fetchStatus: 'fetching',
}
case 'fetch':
return {
...state,
fetchFailureCount: 0,
fetchMeta: action.meta ?? null,
- isFetching: true,
- isPaused: false,
+ fetchStatus: canFetch(this.options.networkMode)
+ ? 'fetching'
+ : 'paused',
status: !state.dataUpdatedAt ? 'loading' : state.status,
}
case 'success':
@@ -594,9 +571,8 @@ export class Query<
dataUpdatedAt: action.dataUpdatedAt ?? Date.now(),
error: null,
fetchFailureCount: 0,
- isFetching: false,
isInvalidated: false,
- isPaused: false,
+ fetchStatus: 'idle',
status: 'success',
}
case 'error':
@@ -612,8 +588,7 @@ export class Query<
errorUpdateCount: state.errorUpdateCount + 1,
errorUpdatedAt: Date.now(),
fetchFailureCount: state.fetchFailureCount + 1,
- isFetching: false,
- isPaused: false,
+ fetchStatus: 'idle',
status: 'error',
}
case 'invalidate':
diff --git a/src/core/queryCache.ts b/src/core/queryCache.ts
index f57b970ead..a5dadc7022 100644
--- a/src/core/queryCache.ts
+++ b/src/core/queryCache.ts
@@ -8,7 +8,7 @@ import { Action, Query, QueryState } from './query'
import type { QueryKey, QueryOptions } from './types'
import { notifyManager } from './notifyManager'
import type { QueryClient } from './queryClient'
-import { Subscribable } from './subscribable'
+import { Notifiable } from './notifiable'
import { QueryObserver } from './queryObserver'
// TYPES
@@ -23,34 +23,34 @@ interface QueryHashMap {
}
interface NotifyEventQueryAdded {
- type: 'queryAdded'
+ type: 'added'
query: Query
}
interface NotifyEventQueryRemoved {
- type: 'queryRemoved'
+ type: 'removed'
query: Query
}
interface NotifyEventQueryUpdated {
- type: 'queryUpdated'
+ type: 'updated'
query: Query
action: Action
}
-interface NotifyEventObserverAdded {
+interface NotifyEventQueryObserverAdded {
type: 'observerAdded'
query: Query
observer: QueryObserver
}
-interface NotifyEventObserverRemoved {
+interface NotifyEventQueryObserverRemoved {
type: 'observerRemoved'
query: Query
observer: QueryObserver
}
-interface NotifyEventObserverResultsUpdated {
+interface NotifyEventQueryObserverResultsUpdated {
type: 'observerResultsUpdated'
query: Query
}
@@ -59,15 +59,13 @@ type QueryCacheNotifyEvent =
| NotifyEventQueryAdded
| NotifyEventQueryRemoved
| NotifyEventQueryUpdated
- | NotifyEventObserverAdded
- | NotifyEventObserverRemoved
- | NotifyEventObserverResultsUpdated
-
-type QueryCacheListener = (event?: QueryCacheNotifyEvent) => void
+ | NotifyEventQueryObserverAdded
+ | NotifyEventQueryObserverRemoved
+ | NotifyEventQueryObserverResultsUpdated
// CLASS
-export class QueryCache extends Subscribable {
+export class QueryCache extends Notifiable {
config: QueryCacheConfig
private queries: Query[]
@@ -111,7 +109,7 @@ export class QueryCache extends Subscribable {
this.queriesMap[query.queryHash] = query
this.queries.push(query)
this.notify({
- type: 'queryAdded',
+ type: 'added',
query,
})
}
@@ -129,7 +127,7 @@ export class QueryCache extends Subscribable {
delete this.queriesMap[query.queryHash]
}
- this.notify({ type: 'queryRemoved', query })
+ this.notify({ type: 'removed', query })
}
}
@@ -179,14 +177,6 @@ export class QueryCache extends Subscribable {
: this.queries
}
- notify(event: QueryCacheNotifyEvent) {
- notifyManager.batch(() => {
- this.listeners.forEach(listener => {
- listener(event)
- })
- })
- }
-
onFocus(): void {
notifyManager.batch(() => {
this.queries.forEach(query => {
diff --git a/src/core/queryClient.ts b/src/core/queryClient.ts
index cb9d6ba181..eae1f3302d 100644
--- a/src/core/queryClient.ts
+++ b/src/core/queryClient.ts
@@ -36,7 +36,7 @@ import { focusManager } from './focusManager'
import { onlineManager } from './onlineManager'
import { notifyManager } from './notifyManager'
import { infiniteQueryBehavior } from './infiniteQueryBehavior'
-import { CancelOptions } from './types'
+import { CancelOptions, DefaultedQueryObserverOptions } from './types'
// TYPES
@@ -77,13 +77,13 @@ export class QueryClient {
mount(): void {
this.unsubscribeFocus = focusManager.subscribe(() => {
- if (focusManager.isFocused() && onlineManager.isOnline()) {
+ if (focusManager.isFocused()) {
this.mutationCache.onFocus()
this.queryCache.onFocus()
}
})
this.unsubscribeOnline = onlineManager.subscribe(() => {
- if (focusManager.isFocused() && onlineManager.isOnline()) {
+ if (onlineManager.isOnline()) {
this.mutationCache.onOnline()
this.queryCache.onOnline()
}
@@ -99,7 +99,7 @@ export class QueryClient {
isFetching(queryKey?: QueryKey, filters?: QueryFilters): number
isFetching(arg1?: QueryKey | QueryFilters, arg2?: QueryFilters): number {
const [filters] = parseFilterArgs(arg1, arg2)
- filters.fetching = true
+ filters.fetchStatus = 'fetching'
return this.queryCache.findAll(filters).length
}
@@ -136,7 +136,7 @@ export class QueryClient {
const defaultedOptions = this.defaultQueryOptions(parsedOptions)
return this.queryCache
.build(this, defaultedOptions)
- .setData(updater, options)
+ .setData(updater, { ...options, notifySuccess: false })
}
setQueriesData(
@@ -203,8 +203,8 @@ export class QueryClient {
const queryCache = this.queryCache
const refetchFilters: RefetchQueryFilters = {
+ type: 'active',
...filters,
- active: true,
}
return notifyManager.batch(() => {
@@ -255,18 +255,18 @@ export class QueryClient {
): Promise {
const [filters, options] = parseFilterArgs(arg1, arg2, arg3)
- const refetchFilters: RefetchQueryFilters = {
- ...filters,
- // if filters.refetchActive is not provided and filters.active is explicitly false,
- // e.g. invalidateQueries({ active: false }), we don't want to refetch active queries
- active: filters.refetchActive ?? filters.active ?? true,
- inactive: filters.refetchInactive ?? false,
- }
-
return notifyManager.batch(() => {
this.queryCache.findAll(filters).forEach(query => {
query.invalidate()
})
+
+ if (filters?.refetchType === 'none') {
+ return Promise.resolve()
+ }
+ const refetchFilters: RefetchQueryFilters = {
+ ...filters,
+ type: filters?.refetchType ?? filters?.type ?? 'active',
+ }
return this.refetchQueries(refetchFilters, options)
})
}
@@ -291,6 +291,7 @@ export class QueryClient {
this.queryCache.findAll(filters).map(query =>
query.fetch(undefined, {
...options,
+ cancelRefetch: options?.cancelRefetch ?? true,
meta: { refetchPage: filters?.refetchPage },
})
)
@@ -499,28 +500,10 @@ export class QueryClient {
.catch(noop)
}
- cancelMutations(): Promise {
- const promises = notifyManager.batch(() =>
- this.mutationCache.getAll().map(mutation => mutation.cancel())
- )
- return Promise.all(promises).then(noop).catch(noop)
- }
-
resumePausedMutations(): Promise {
return this.getMutationCache().resumePausedMutations()
}
- executeMutation<
- TData = unknown,
- TError = unknown,
- TVariables = void,
- TContext = unknown
- >(
- options: MutationOptions
- ): Promise {
- return this.mutationCache.build(this, options).execute()
- }
-
getQueryCache(): QueryCache {
return this.queryCache
}
@@ -591,16 +574,30 @@ export class QueryClient {
TQueryData,
TQueryKey extends QueryKey
>(
- options?: QueryObserverOptions<
- TQueryFnData,
- TError,
- TData,
- TQueryData,
- TQueryKey
- >
- ): QueryObserverOptions {
+ options?:
+ | QueryObserverOptions
+ | DefaultedQueryObserverOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryData,
+ TQueryKey
+ >
+ ): DefaultedQueryObserverOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryData,
+ TQueryKey
+ > {
if (options?._defaulted) {
- return options
+ return options as DefaultedQueryObserverOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryData,
+ TQueryKey
+ >
}
const defaultedOptions = {
@@ -608,13 +605,7 @@ export class QueryClient {
...this.getQueryDefaults(options?.queryKey),
...options,
_defaulted: true,
- } as QueryObserverOptions<
- TQueryFnData,
- TError,
- TData,
- TQueryData,
- TQueryKey
- >
+ }
if (!defaultedOptions.queryHash && defaultedOptions.queryKey) {
defaultedOptions.queryHash = hashQueryKeyByOptions(
@@ -623,25 +614,22 @@ export class QueryClient {
)
}
- return defaultedOptions
- }
+ // dependent default values
+ if (typeof defaultedOptions.refetchOnReconnect === 'undefined') {
+ defaultedOptions.refetchOnReconnect =
+ defaultedOptions.networkMode !== 'always'
+ }
+ if (typeof defaultedOptions.useErrorBoundary === 'undefined') {
+ defaultedOptions.useErrorBoundary = !!defaultedOptions.suspense
+ }
- defaultQueryObserverOptions<
- TQueryFnData,
- TError,
- TData,
- TQueryData,
- TQueryKey extends QueryKey
- >(
- options?: QueryObserverOptions<
+ return defaultedOptions as DefaultedQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>
- ): QueryObserverOptions {
- return this.defaultQueryOptions(options)
}
defaultMutationOptions>(
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index aa36cfd2cd..6ed455d60f 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -1,4 +1,4 @@
-import { RefetchQueryFilters } from './types'
+import { DefaultedQueryObserverOptions, RefetchPageFilters } from './types'
import {
isServer,
isValidTimeout,
@@ -23,7 +23,7 @@ import type { QueryClient } from './queryClient'
import { focusManager } from './focusManager'
import { Subscribable } from './subscribable'
import { getLogger } from './logger'
-import { isCancelledError } from './retryer'
+import { canFetch, isCancelledError } from './retryer'
type QueryObserverListener = (
result: QueryObserverResult
@@ -144,7 +144,7 @@ export class QueryObserver<
const prevOptions = this.options
const prevQuery = this.currentQuery
- this.options = this.client.defaultQueryObserverOptions(options)
+ this.options = this.client.defaultQueryOptions(options)
if (
typeof this.options.enabled !== 'undefined' &&
@@ -210,19 +210,11 @@ export class QueryObserver<
TQueryKey
>
): QueryObserverResult {
- const defaultedOptions = this.client.defaultQueryObserverOptions(options)
+ const defaultedOptions = this.client.defaultQueryOptions(options)
const query = this.client
.getQueryCache()
- .build(
- this.client,
- defaultedOptions as QueryOptions<
- TQueryFnData,
- TError,
- TQueryData,
- TQueryKey
- >
- )
+ .build(this.client, defaultedOptions)
return this.createResult(query, defaultedOptions)
}
@@ -233,7 +225,7 @@ export class QueryObserver<
trackResult(
result: QueryObserverResult,
- defaultedOptions: QueryObserverOptions<
+ defaultedOptions: DefaultedQueryObserverOptions<
TQueryFnData,
TError,
TData,
@@ -260,7 +252,7 @@ export class QueryObserver<
})
})
- if (defaultedOptions.useErrorBoundary || defaultedOptions.suspense) {
+ if (defaultedOptions.useErrorBoundary) {
trackProp('error')
}
@@ -292,12 +284,15 @@ export class QueryObserver<
this.client.getQueryCache().remove(this.currentQuery)
}
- refetch(
- options?: RefetchOptions & RefetchQueryFilters
- ): Promise> {
+ refetch({
+ refetchPage,
+ ...options
+ }: RefetchOptions & RefetchPageFilters = {}): Promise<
+ QueryObserverResult
+ > {
return this.fetch({
...options,
- meta: { refetchPage: options?.refetchPage },
+ meta: { refetchPage },
})
}
@@ -310,27 +305,23 @@ export class QueryObserver<
TQueryKey
>
): Promise> {
- const defaultedOptions = this.client.defaultQueryObserverOptions(options)
+ const defaultedOptions = this.client.defaultQueryOptions(options)
const query = this.client
.getQueryCache()
- .build(
- this.client,
- defaultedOptions as QueryOptions<
- TQueryFnData,
- TError,
- TQueryData,
- TQueryKey
- >
- )
+ .build(this.client, defaultedOptions)
+ query.isFetchingOptimistic = true
return query.fetch().then(() => this.createResult(query, defaultedOptions))
}
protected fetch(
- fetchOptions?: ObserverFetchOptions
+ fetchOptions: ObserverFetchOptions
): Promise> {
- return this.executeFetch(fetchOptions).then(() => {
+ return this.executeFetch({
+ ...fetchOptions,
+ cancelRefetch: fetchOptions.cancelRefetch ?? true,
+ }).then(() => {
this.updateResult()
return this.currentResult
})
@@ -456,7 +447,7 @@ export class QueryObserver<
: this.previousQueryResult
const { state } = query
- let { dataUpdatedAt, error, errorUpdatedAt, isFetching, status } = state
+ let { dataUpdatedAt, error, errorUpdatedAt, fetchStatus, status } = state
let isPreviousData = false
let isPlaceholderData = false
let data: TData | undefined
@@ -471,7 +462,9 @@ export class QueryObserver<
mounted && shouldFetchOptionally(query, prevQuery, options, prevOptions)
if (fetchOnMount || fetchOptionally) {
- isFetching = true
+ fetchStatus = canFetch(query.options.networkMode)
+ ? 'fetching'
+ : 'paused'
if (!dataUpdatedAt) {
status = 'loading'
}
@@ -567,8 +560,11 @@ export class QueryObserver<
}
}
+ const isFetching = fetchStatus === 'fetching'
+
const result: QueryObserverBaseResult = {
status,
+ fetchStatus,
isLoading: status === 'loading',
isSuccess: status === 'success',
isError: status === 'error',
@@ -582,9 +578,10 @@ export class QueryObserver<
isFetchedAfterMount:
state.dataUpdateCount > queryInitialState.dataUpdateCount ||
state.errorUpdateCount > queryInitialState.errorUpdateCount,
- isFetching,
+ isFetching: isFetching,
isRefetching: isFetching && status !== 'loading',
isLoadingError: status === 'error' && state.dataUpdatedAt === 0,
+ isPaused: fetchStatus === 'paused',
isPlaceholderData,
isPreviousData,
isRefetchError: status === 'error' && state.dataUpdatedAt !== 0,
@@ -604,27 +601,22 @@ export class QueryObserver<
return true
}
- const { notifyOnChangeProps, notifyOnChangePropsExclusions } = this.options
-
- if (!notifyOnChangeProps && !notifyOnChangePropsExclusions) {
- return true
- }
+ const { notifyOnChangeProps } = this.options
- if (notifyOnChangeProps === 'tracked' && !this.trackedProps.length) {
+ if (
+ notifyOnChangeProps === 'all' ||
+ (!notifyOnChangeProps && !this.trackedProps.length)
+ ) {
return true
}
- const includedProps =
- notifyOnChangeProps === 'tracked'
- ? this.trackedProps
- : notifyOnChangeProps
+ const includedProps = notifyOnChangeProps ?? this.trackedProps
return Object.keys(result).some(key => {
const typedKey = key as keyof QueryObserverResult
const changed = result[typedKey] !== prevResult[typedKey]
const isIncluded = includedProps?.some(x => x === key)
- const isExcluded = notifyOnChangePropsExclusions?.some(x => x === key)
- return changed && !isExcluded && (!includedProps || isIncluded)
+ return changed && (!includedProps || isIncluded)
})
}
@@ -656,17 +648,7 @@ export class QueryObserver<
}
private updateQuery(): void {
- const query = this.client
- .getQueryCache()
- .build(
- this.client,
- this.options as QueryOptions<
- TQueryFnData,
- TError,
- TQueryData,
- TQueryKey
- >
- )
+ const query = this.client.getQueryCache().build(this.client, this.options)
if (query === this.currentQuery) {
return
@@ -687,7 +669,7 @@ export class QueryObserver<
const notifyOptions: NotifyOptions = {}
if (action.type === 'success') {
- notifyOptions.onSuccess = true
+ notifyOptions.onSuccess = action.notifySuccess ?? true
} else if (action.type === 'error' && !isCancelledError(action.error)) {
notifyOptions.onError = true
}
@@ -719,9 +701,10 @@ export class QueryObserver<
// Then the cache listeners
if (notifyOptions.cache) {
- this.client
- .getQueryCache()
- .notify({ query: this.currentQuery, type: 'observerResultsUpdated' })
+ this.client.getQueryCache().notify({
+ query: this.currentQuery,
+ type: 'observerResultsUpdated',
+ })
}
})
}
diff --git a/src/core/removable.ts b/src/core/removable.ts
new file mode 100644
index 0000000000..c33f08202c
--- /dev/null
+++ b/src/core/removable.ts
@@ -0,0 +1,35 @@
+import { isValidTimeout } from './utils'
+
+export abstract class Removable {
+ cacheTime!: number
+ private gcTimeout?: number
+
+ destroy(): void {
+ this.clearGcTimeout()
+ }
+
+ protected scheduleGc(): void {
+ this.clearGcTimeout()
+
+ if (isValidTimeout(this.cacheTime)) {
+ this.gcTimeout = setTimeout(() => {
+ this.optionalRemove()
+ }, this.cacheTime)
+ }
+ }
+
+ protected updateCacheTime(newCacheTime: number | undefined): void {
+ // Default to 5 minutes if no cache time is set
+ this.cacheTime = Math.max(
+ this.cacheTime || 0,
+ newCacheTime ?? 5 * 60 * 1000
+ )
+ }
+
+ protected clearGcTimeout() {
+ clearTimeout(this.gcTimeout)
+ this.gcTimeout = undefined
+ }
+
+ protected abstract optionalRemove(): void
+}
diff --git a/src/core/retryer.ts b/src/core/retryer.ts
index 07621123d4..537f1b28ab 100644
--- a/src/core/retryer.ts
+++ b/src/core/retryer.ts
@@ -1,7 +1,7 @@
import { focusManager } from './focusManager'
import { onlineManager } from './onlineManager'
import { sleep } from './utils'
-import { CancelOptions } from './types'
+import { CancelOptions, NetworkMode } from './types'
// TYPES
@@ -15,11 +15,12 @@ interface RetryerConfig {
onContinue?: () => void
retry?: RetryValue
retryDelay?: RetryDelayValue
+ networkMode: NetworkMode | undefined
}
export type RetryValue = boolean | number | ShouldRetryFunction
-type ShouldRetryFunction = (
+type ShouldRetryFunction = (
failureCount: number,
error: TError
) => boolean
@@ -35,12 +36,10 @@ function defaultRetryDelay(failureCount: number) {
return Math.min(1000 * 2 ** failureCount, 30000)
}
-interface Cancelable {
- cancel(): void
-}
-
-export function isCancelable(value: any): value is Cancelable {
- return typeof value?.cancel === 'function'
+export function canFetch(networkMode: NetworkMode | undefined): boolean {
+ return (networkMode ?? 'online') === 'online'
+ ? onlineManager.isOnline()
+ : true
}
export class CancelledError {
@@ -66,31 +65,39 @@ export class Retryer {
failureCount: number
isPaused: boolean
isResolved: boolean
- isTransportCancelable: boolean
promise: Promise
- private abort?: () => void
-
constructor(config: RetryerConfig) {
let cancelRetry = false
- let cancelFn: ((options?: CancelOptions) => void) | undefined
let continueFn: ((value?: unknown) => void) | undefined
let promiseResolve: (data: TData) => void
let promiseReject: (error: TError) => void
- this.abort = config.abort
- this.cancel = cancelOptions => cancelFn?.(cancelOptions)
+ this.cancel = (cancelOptions?: CancelOptions): void => {
+ if (!this.isResolved) {
+ reject(new CancelledError(cancelOptions))
+
+ config.abort?.()
+ }
+ }
this.cancelRetry = () => {
cancelRetry = true
}
+
this.continueRetry = () => {
cancelRetry = false
}
- this.continue = () => continueFn?.()
+
+ const shouldPause = () =>
+ !focusManager.isFocused() ||
+ (config.networkMode !== 'always' && !onlineManager.isOnline())
+
+ this.continue = () => {
+ continueFn?.()
+ }
this.failureCount = 0
this.isPaused = false
this.isResolved = false
- this.isTransportCancelable = false
this.promise = new Promise((outerResolve, outerReject) => {
promiseResolve = outerResolve
promiseReject = outerReject
@@ -116,13 +123,19 @@ export class Retryer {
const pause = () => {
return new Promise(continueResolve => {
- continueFn = continueResolve
+ continueFn = value => {
+ if (this.isResolved || !shouldPause()) {
+ return continueResolve(value)
+ }
+ }
this.isPaused = true
config.onPause?.()
}).then(() => {
continueFn = undefined
this.isPaused = false
- config.onContinue?.()
+ if (!this.isResolved) {
+ config.onContinue?.()
+ }
})
}
@@ -142,25 +155,6 @@ export class Retryer {
promiseOrValue = Promise.reject(error)
}
- // Create callback to cancel this fetch
- cancelFn = cancelOptions => {
- if (!this.isResolved) {
- reject(new CancelledError(cancelOptions))
-
- this.abort?.()
-
- // Cancel transport if supported
- if (isCancelable(promiseOrValue)) {
- try {
- promiseOrValue.cancel()
- } catch {}
- }
- }
- }
-
- // Check if the transport layer support cancellation
- this.isTransportCancelable = isCancelable(promiseOrValue)
-
Promise.resolve(promiseOrValue)
.then(resolve)
.catch(error => {
@@ -196,7 +190,7 @@ export class Retryer {
sleep(delay)
// Pause if the document is not visible or when the device is offline
.then(() => {
- if (!focusManager.isFocused() || !onlineManager.isOnline()) {
+ if (shouldPause()) {
return pause()
}
})
@@ -211,6 +205,10 @@ export class Retryer {
}
// Start loop
- run()
+ if (canFetch(config.networkMode)) {
+ run()
+ } else {
+ pause().then(run)
+ }
}
}
diff --git a/src/core/tests/hydration.test.tsx b/src/core/tests/hydration.test.tsx
index 2de31d1132..77a5e58924 100644
--- a/src/core/tests/hydration.test.tsx
+++ b/src/core/tests/hydration.test.tsx
@@ -1,4 +1,8 @@
-import { mockNavigatorOnLine, sleep } from '../../react/tests/utils'
+import {
+ executeMutation,
+ mockNavigatorOnLine,
+ sleep,
+} from '../../reactjs/tests/utils'
import { QueryCache } from '../queryCache'
import { QueryClient } from '../queryClient'
import { dehydrate, hydrate } from '../hydration'
@@ -12,12 +16,12 @@ describe('dehydration and rehydration', () => {
test('should work with serializeable values', async () => {
const queryCache = new QueryCache()
const queryClient = new QueryClient({ queryCache })
- await queryClient.prefetchQuery('string', () => fetchData('string'))
- await queryClient.prefetchQuery('number', () => fetchData(1))
- await queryClient.prefetchQuery('boolean', () => fetchData(true))
- await queryClient.prefetchQuery('null', () => fetchData(null))
- await queryClient.prefetchQuery('array', () => fetchData(['string', 0]))
- await queryClient.prefetchQuery('nested', () =>
+ await queryClient.prefetchQuery(['string'], () => fetchData('string'))
+ await queryClient.prefetchQuery(['number'], () => fetchData(1))
+ await queryClient.prefetchQuery(['boolean'], () => fetchData(true))
+ await queryClient.prefetchQuery(['null'], () => fetchData(null))
+ await queryClient.prefetchQuery(['array'], () => fetchData(['string', 0]))
+ await queryClient.prefetchQuery(['nested'], () =>
fetchData({ key: [{ nestedKey: 1 }] })
)
const dehydrated = dehydrate(queryClient)
@@ -29,32 +33,32 @@ describe('dehydration and rehydration', () => {
const hydrationCache = new QueryCache()
const hydrationClient = new QueryClient({ queryCache: hydrationCache })
hydrate(hydrationClient, parsed)
- expect(hydrationCache.find('string')?.state.data).toBe('string')
- expect(hydrationCache.find('number')?.state.data).toBe(1)
- expect(hydrationCache.find('boolean')?.state.data).toBe(true)
- expect(hydrationCache.find('null')?.state.data).toBe(null)
- expect(hydrationCache.find('array')?.state.data).toEqual(['string', 0])
- expect(hydrationCache.find('nested')?.state.data).toEqual({
+ expect(hydrationCache.find(['string'])?.state.data).toBe('string')
+ expect(hydrationCache.find(['number'])?.state.data).toBe(1)
+ expect(hydrationCache.find(['boolean'])?.state.data).toBe(true)
+ expect(hydrationCache.find(['null'])?.state.data).toBe(null)
+ expect(hydrationCache.find(['array'])?.state.data).toEqual(['string', 0])
+ expect(hydrationCache.find(['nested'])?.state.data).toEqual({
key: [{ nestedKey: 1 }],
})
const fetchDataAfterHydration = jest.fn()
- await hydrationClient.prefetchQuery('string', fetchDataAfterHydration, {
+ await hydrationClient.prefetchQuery(['string'], fetchDataAfterHydration, {
staleTime: 1000,
})
- await hydrationClient.prefetchQuery('number', fetchDataAfterHydration, {
+ await hydrationClient.prefetchQuery(['number'], fetchDataAfterHydration, {
staleTime: 1000,
})
- await hydrationClient.prefetchQuery('boolean', fetchDataAfterHydration, {
+ await hydrationClient.prefetchQuery(['boolean'], fetchDataAfterHydration, {
staleTime: 1000,
})
- await hydrationClient.prefetchQuery('null', fetchDataAfterHydration, {
+ await hydrationClient.prefetchQuery(['null'], fetchDataAfterHydration, {
staleTime: 1000,
})
- await hydrationClient.prefetchQuery('array', fetchDataAfterHydration, {
+ await hydrationClient.prefetchQuery(['array'], fetchDataAfterHydration, {
staleTime: 1000,
})
- await hydrationClient.prefetchQuery('nested', fetchDataAfterHydration, {
+ await hydrationClient.prefetchQuery(['nested'], fetchDataAfterHydration, {
staleTime: 1000,
})
expect(fetchDataAfterHydration).toHaveBeenCalledTimes(0)
@@ -66,7 +70,7 @@ describe('dehydration and rehydration', () => {
test('should not dehydrate queries if dehydrateQueries is set to false', async () => {
const queryCache = new QueryCache()
const queryClient = new QueryClient({ queryCache })
- await queryClient.prefetchQuery('string', () => fetchData('string'))
+ await queryClient.prefetchQuery(['string'], () => fetchData('string'))
const dehydrated = dehydrate(queryClient, { dehydrateQueries: false })
@@ -78,7 +82,7 @@ describe('dehydration and rehydration', () => {
test('should use the cache time from the client', async () => {
const queryCache = new QueryCache()
const queryClient = new QueryClient({ queryCache })
- await queryClient.prefetchQuery('string', () => fetchData('string'), {
+ await queryClient.prefetchQuery(['string'], () => fetchData('string'), {
cacheTime: 50,
})
const dehydrated = dehydrate(queryClient)
@@ -92,9 +96,9 @@ describe('dehydration and rehydration', () => {
const hydrationCache = new QueryCache()
const hydrationClient = new QueryClient({ queryCache: hydrationCache })
hydrate(hydrationClient, parsed)
- expect(hydrationCache.find('string')?.state.data).toBe('string')
+ expect(hydrationCache.find(['string'])?.state.data).toBe('string')
await sleep(100)
- expect(hydrationCache.find('string')).toBeTruthy()
+ expect(hydrationCache.find(['string'])).toBeTruthy()
queryClient.clear()
hydrationClient.clear()
@@ -103,7 +107,7 @@ describe('dehydration and rehydration', () => {
test('should be able to provide default options for the hydrated queries', async () => {
const queryCache = new QueryCache()
const queryClient = new QueryClient({ queryCache })
- await queryClient.prefetchQuery('string', () => fetchData('string'))
+ await queryClient.prefetchQuery(['string'], () => fetchData('string'))
const dehydrated = dehydrate(queryClient)
const stringified = JSON.stringify(dehydrated)
const parsed = JSON.parse(stringified)
@@ -112,7 +116,7 @@ describe('dehydration and rehydration', () => {
hydrate(hydrationClient, parsed, {
defaultOptions: { queries: { retry: 10 } },
})
- expect(hydrationCache.find('string')?.options.retry).toBe(10)
+ expect(hydrationCache.find(['string'])?.options.retry).toBe(10)
queryClient.clear()
hydrationClient.clear()
})
@@ -155,9 +159,9 @@ describe('dehydration and rehydration', () => {
const queryCache = new QueryCache()
const queryClient = new QueryClient({ queryCache })
- await queryClient.prefetchQuery('success', () => fetchData('success'))
- queryClient.prefetchQuery('loading', () => fetchData('loading', 10000))
- await queryClient.prefetchQuery('error', () => {
+ await queryClient.prefetchQuery(['success'], () => fetchData('success'))
+ queryClient.prefetchQuery(['loading'], () => fetchData('loading', 10000))
+ await queryClient.prefetchQuery(['error'], () => {
throw new Error()
})
const dehydrated = dehydrate(queryClient)
@@ -170,9 +174,9 @@ describe('dehydration and rehydration', () => {
const hydrationClient = new QueryClient({ queryCache: hydrationCache })
hydrate(hydrationClient, parsed)
- expect(hydrationCache.find('success')).toBeTruthy()
- expect(hydrationCache.find('loading')).toBeFalsy()
- expect(hydrationCache.find('error')).toBeFalsy()
+ expect(hydrationCache.find(['success'])).toBeTruthy()
+ expect(hydrationCache.find(['loading'])).toBeFalsy()
+ expect(hydrationCache.find(['error'])).toBeFalsy()
queryClient.clear()
hydrationClient.clear()
@@ -182,16 +186,16 @@ describe('dehydration and rehydration', () => {
test('should filter queries via shouldDehydrateQuery', async () => {
const queryCache = new QueryCache()
const queryClient = new QueryClient({ queryCache })
- await queryClient.prefetchQuery('string', () => fetchData('string'))
- await queryClient.prefetchQuery('number', () => fetchData(1))
+ await queryClient.prefetchQuery(['string'], () => fetchData('string'))
+ await queryClient.prefetchQuery(['number'], () => fetchData(1))
const dehydrated = dehydrate(queryClient, {
- shouldDehydrateQuery: query => query.queryKey !== 'string',
+ shouldDehydrateQuery: query => query.queryKey[0] !== 'string',
})
// This is testing implementation details that can change and are not
// part of the public API, but is important for keeping the payload small
const dehydratedQuery = dehydrated?.queries.find(
- query => query?.queryKey === 'string'
+ query => query?.queryKey[0] === 'string'
)
expect(dehydratedQuery).toBeUndefined()
@@ -203,8 +207,8 @@ describe('dehydration and rehydration', () => {
const hydrationCache = new QueryCache()
const hydrationClient = new QueryClient({ queryCache: hydrationCache })
hydrate(hydrationClient, parsed)
- expect(hydrationCache.find('string')).toBeUndefined()
- expect(hydrationCache.find('number')?.state.data).toBe(1)
+ expect(hydrationCache.find(['string'])).toBeUndefined()
+ expect(hydrationCache.find(['number'])?.state.data).toBe(1)
queryClient.clear()
hydrationClient.clear()
@@ -213,7 +217,7 @@ describe('dehydration and rehydration', () => {
test('should not overwrite query in cache if hydrated query is older', async () => {
const queryCache = new QueryCache()
const queryClient = new QueryClient({ queryCache })
- await queryClient.prefetchQuery('string', () =>
+ await queryClient.prefetchQuery(['string'], () =>
fetchData('string-older', 5)
)
const dehydrated = dehydrate(queryClient)
@@ -224,12 +228,12 @@ describe('dehydration and rehydration', () => {
const parsed = JSON.parse(stringified)
const hydrationCache = new QueryCache()
const hydrationClient = new QueryClient({ queryCache: hydrationCache })
- await hydrationClient.prefetchQuery('string', () =>
+ await hydrationClient.prefetchQuery(['string'], () =>
fetchData('string-newer', 5)
)
hydrate(hydrationClient, parsed)
- expect(hydrationCache.find('string')?.state.data).toBe('string-newer')
+ expect(hydrationCache.find(['string'])?.state.data).toBe('string-newer')
queryClient.clear()
hydrationClient.clear()
@@ -238,7 +242,7 @@ describe('dehydration and rehydration', () => {
test('should overwrite query in cache if hydrated query is newer', async () => {
const hydrationCache = new QueryCache()
const hydrationClient = new QueryClient({ queryCache: hydrationCache })
- await hydrationClient.prefetchQuery('string', () =>
+ await hydrationClient.prefetchQuery(['string'], () =>
fetchData('string-older', 5)
)
@@ -246,7 +250,7 @@ describe('dehydration and rehydration', () => {
const queryCache = new QueryCache()
const queryClient = new QueryClient({ queryCache })
- await queryClient.prefetchQuery('string', () =>
+ await queryClient.prefetchQuery(['string'], () =>
fetchData('string-newer', 5)
)
const dehydrated = dehydrate(queryClient)
@@ -256,7 +260,7 @@ describe('dehydration and rehydration', () => {
const parsed = JSON.parse(stringified)
hydrate(hydrationClient, parsed)
- expect(hydrationCache.find('string')?.state.data).toBe('string-newer')
+ expect(hydrationCache.find(['string'])?.state.data).toBe('string-newer')
queryClient.clear()
hydrationClient.clear()
@@ -265,7 +269,7 @@ describe('dehydration and rehydration', () => {
test('should be able to dehydrate mutations and continue on hydration', async () => {
const consoleMock = jest.spyOn(console, 'error')
consoleMock.mockImplementation(() => undefined)
- mockNavigatorOnLine(false)
+ const onlineMock = mockNavigatorOnLine(false)
const serverAddTodo = jest
.fn()
@@ -278,7 +282,7 @@ describe('dehydration and rehydration', () => {
const serverClient = new QueryClient()
- serverClient.setMutationDefaults('addTodo', {
+ serverClient.setMutationDefaults(['addTodo'], {
mutationFn: serverAddTodo,
onMutate: serverOnMutate,
onSuccess: serverOnSuccess,
@@ -286,12 +290,10 @@ describe('dehydration and rehydration', () => {
retryDelay: 10,
})
- serverClient
- .executeMutation({
- mutationKey: 'addTodo',
- variables: { text: 'text' },
- })
- .catch(() => undefined)
+ executeMutation(serverClient, {
+ mutationKey: ['addTodo'],
+ variables: { text: 'text' },
+ }).catch(() => undefined)
await sleep(50)
@@ -302,7 +304,7 @@ describe('dehydration and rehydration', () => {
// ---
- mockNavigatorOnLine(true)
+ onlineMock.mockReturnValue(true)
const parsed = JSON.parse(stringified)
const client = new QueryClient()
@@ -316,7 +318,7 @@ describe('dehydration and rehydration', () => {
})
const clientOnSuccess = jest.fn()
- client.setMutationDefaults('addTodo', {
+ client.setMutationDefaults(['addTodo'], {
mutationFn: clientAddTodo,
onMutate: clientOnMutate,
onSuccess: clientOnSuccess,
@@ -339,6 +341,7 @@ describe('dehydration and rehydration', () => {
client.clear()
consoleMock.mockRestore()
+ onlineMock.mockRestore()
})
test('should not dehydrate mutations if dehydrateMutations is set to false', async () => {
@@ -351,17 +354,15 @@ describe('dehydration and rehydration', () => {
const queryClient = new QueryClient()
- queryClient.setMutationDefaults('addTodo', {
+ queryClient.setMutationDefaults(['addTodo'], {
mutationFn: serverAddTodo,
retry: false,
})
- queryClient
- .executeMutation({
- mutationKey: 'addTodo',
- variables: { text: 'text' },
- })
- .catch(() => undefined)
+ executeMutation(queryClient, {
+ mutationKey: ['addTodo'],
+ variables: { text: 'text' },
+ }).catch(() => undefined)
await sleep(1)
const dehydrated = dehydrate(queryClient, { dehydrateMutations: false })
@@ -382,18 +383,16 @@ describe('dehydration and rehydration', () => {
const queryClient = new QueryClient()
- queryClient.setMutationDefaults('addTodo', {
+ queryClient.setMutationDefaults(['addTodo'], {
mutationFn: serverAddTodo,
retry: 1,
retryDelay: 20,
})
- queryClient
- .executeMutation({
- mutationKey: 'addTodo',
- variables: { text: 'text' },
- })
- .catch(() => undefined)
+ executeMutation(queryClient, {
+ mutationKey: ['addTodo'],
+ variables: { text: 'text' },
+ }).catch(() => undefined)
// Dehydrate mutation between retries
await sleep(1)
diff --git a/src/core/tests/infiniteQueryBehavior.test.tsx b/src/core/tests/infiniteQueryBehavior.test.tsx
index 92d5c6c9f9..a3e57e30e4 100644
--- a/src/core/tests/infiniteQueryBehavior.test.tsx
+++ b/src/core/tests/infiniteQueryBehavior.test.tsx
@@ -1,5 +1,5 @@
import { waitFor } from '@testing-library/react'
-import { queryKey, mockConsoleError } from '../../react/tests/utils'
+import { queryKey, mockConsoleError } from '../../reactjs/tests/utils'
import {
QueryClient,
InfiniteQueryObserver,
@@ -80,7 +80,7 @@ describe('InfiniteQueryBehavior', () => {
)
expect(queryFnSpy).toHaveBeenNthCalledWith(1, {
- queryKey: [key],
+ queryKey: key,
pageParam: undefined,
meta: undefined,
signal: abortSignal,
@@ -92,7 +92,7 @@ describe('InfiniteQueryBehavior', () => {
await observer.fetchNextPage()
expect(queryFnSpy).toHaveBeenNthCalledWith(1, {
- queryKey: [key],
+ queryKey: key,
pageParam: 2,
meta: undefined,
signal: abortSignal,
@@ -111,7 +111,7 @@ describe('InfiniteQueryBehavior', () => {
})
expect(queryFnSpy).toHaveBeenNthCalledWith(1, {
- queryKey: [key],
+ queryKey: key,
pageParam: 2,
meta: undefined,
signal: abortSignal,
diff --git a/src/core/tests/infiniteQueryObserver.test.tsx b/src/core/tests/infiniteQueryObserver.test.tsx
index e0fff2a28b..27094fe615 100644
--- a/src/core/tests/infiniteQueryObserver.test.tsx
+++ b/src/core/tests/infiniteQueryObserver.test.tsx
@@ -1,4 +1,4 @@
-import { sleep, queryKey } from '../../react/tests/utils'
+import { sleep, queryKey } from '../../reactjs/tests/utils'
import { QueryClient, InfiniteQueryObserver } from '../..'
describe('InfiniteQueryObserver', () => {
diff --git a/src/core/tests/mutationCache.test.tsx b/src/core/tests/mutationCache.test.tsx
index c0bd84f487..ff3ca5cea5 100644
--- a/src/core/tests/mutationCache.test.tsx
+++ b/src/core/tests/mutationCache.test.tsx
@@ -1,5 +1,11 @@
-import { queryKey, mockConsoleError } from '../../react/tests/utils'
-import { MutationCache, QueryClient } from '../..'
+import { waitFor } from '@testing-library/react'
+import {
+ queryKey,
+ mockConsoleError,
+ sleep,
+ executeMutation,
+} from '../../reactjs/tests/utils'
+import { MutationCache, MutationObserver, QueryClient } from '../..'
describe('mutationCache', () => {
describe('MutationCacheConfig.onError', () => {
@@ -11,7 +17,7 @@ describe('mutationCache', () => {
const testClient = new QueryClient({ mutationCache: testCache })
try {
- await testClient.executeMutation({
+ await executeMutation(testClient, {
mutationKey: key,
variables: 'vars',
mutationFn: () => Promise.reject('error'),
@@ -34,7 +40,7 @@ describe('mutationCache', () => {
const testClient = new QueryClient({ mutationCache: testCache })
try {
- await testClient.executeMutation({
+ await executeMutation(testClient, {
mutationKey: key,
variables: 'vars',
mutationFn: () => Promise.resolve({ data: 5 }),
@@ -62,7 +68,7 @@ describe('mutationCache', () => {
const testClient = new QueryClient({ mutationCache: testCache })
try {
- await testClient.executeMutation({
+ await executeMutation(testClient, {
mutationKey: key,
variables: 'vars',
mutationFn: () => Promise.resolve({ data: 5 }),
@@ -82,17 +88,17 @@ describe('mutationCache', () => {
const testCache = new MutationCache()
const testClient = new QueryClient({ mutationCache: testCache })
const key = ['mutation', 'vars']
- await testClient.executeMutation({
+ await executeMutation(testClient, {
mutationKey: key,
variables: 'vars',
mutationFn: () => Promise.resolve(),
})
const [mutation] = testCache.getAll()
expect(testCache.find({ mutationKey: key })).toEqual(mutation)
- expect(testCache.find({ mutationKey: 'mutation', exact: false })).toEqual(
- mutation
- )
- expect(testCache.find({ mutationKey: 'unknown' })).toEqual(undefined)
+ expect(
+ testCache.find({ mutationKey: ['mutation'], exact: false })
+ ).toEqual(mutation)
+ expect(testCache.find({ mutationKey: ['unknown'] })).toEqual(undefined)
expect(
testCache.find({ predicate: m => m.options.variables === 'vars' })
).toEqual(mutation)
@@ -103,30 +109,161 @@ describe('mutationCache', () => {
test('should filter correctly', async () => {
const testCache = new MutationCache()
const testClient = new QueryClient({ mutationCache: testCache })
- await testClient.executeMutation({
+ await executeMutation(testClient, {
mutationKey: ['a', 1],
variables: 1,
mutationFn: () => Promise.resolve(),
})
- await testClient.executeMutation({
+ await executeMutation(testClient, {
mutationKey: ['a', 2],
variables: 2,
mutationFn: () => Promise.resolve(),
})
- await testClient.executeMutation({
- mutationKey: 'b',
+ await executeMutation(testClient, {
+ mutationKey: ['b'],
mutationFn: () => Promise.resolve(),
})
const [mutation1, mutation2] = testCache.getAll()
expect(
- testCache.findAll({ mutationKey: 'a', exact: false })
+ testCache.findAll({ mutationKey: ['a'], exact: false })
).toHaveLength(2)
expect(testCache.find({ mutationKey: ['a', 1] })).toEqual(mutation1)
- expect(testCache.findAll({ mutationKey: 'unknown' })).toEqual([])
+ expect(testCache.findAll({ mutationKey: ['unknown'] })).toEqual([])
expect(
testCache.findAll({ predicate: m => m.options.variables === 2 })
).toEqual([mutation2])
})
})
+
+ describe('garbage collection', () => {
+ test('should remove unused mutations after cacheTime has elapsed', async () => {
+ const testCache = new MutationCache()
+ const testClient = new QueryClient({ mutationCache: testCache })
+ const onSuccess = jest.fn()
+ await executeMutation(testClient, {
+ mutationKey: ['a', 1],
+ variables: 1,
+ cacheTime: 10,
+ mutationFn: () => Promise.resolve(),
+ onSuccess,
+ })
+
+ expect(testCache.getAll()).toHaveLength(1)
+ await sleep(10)
+ await waitFor(() => {
+ expect(testCache.getAll()).toHaveLength(0)
+ })
+ expect(onSuccess).toHaveBeenCalledTimes(1)
+ })
+
+ test('should not remove mutations if there are active observers', async () => {
+ const queryClient = new QueryClient()
+ const observer = new MutationObserver(queryClient, {
+ variables: 1,
+ cacheTime: 10,
+ mutationFn: () => Promise.resolve(),
+ })
+ const unsubscribe = observer.subscribe()
+
+ expect(queryClient.getMutationCache().getAll()).toHaveLength(0)
+ observer.mutate(1)
+ expect(queryClient.getMutationCache().getAll()).toHaveLength(1)
+ await sleep(10)
+ expect(queryClient.getMutationCache().getAll()).toHaveLength(1)
+ unsubscribe()
+ expect(queryClient.getMutationCache().getAll()).toHaveLength(1)
+ await sleep(10)
+ await waitFor(() => {
+ expect(queryClient.getMutationCache().getAll()).toHaveLength(0)
+ })
+ })
+
+ test('should only remove when the last observer unsubscribes', async () => {
+ const queryClient = new QueryClient()
+ const observer1 = new MutationObserver(queryClient, {
+ variables: 1,
+ cacheTime: 10,
+ mutationFn: async () => {
+ await sleep(10)
+ return 'update1'
+ },
+ })
+
+ const observer2 = new MutationObserver(queryClient, {
+ cacheTime: 10,
+ mutationFn: async () => {
+ await sleep(10)
+ return 'update2'
+ },
+ })
+
+ await observer1.mutate()
+
+ // we currently have no way to add multiple observers to the same mutation
+ const currentMutation = observer1['currentMutation']!
+ currentMutation?.addObserver(observer1)
+ currentMutation?.addObserver(observer2)
+
+ expect(currentMutation['observers'].length).toEqual(2)
+ expect(queryClient.getMutationCache().getAll()).toHaveLength(1)
+
+ currentMutation?.removeObserver(observer1)
+ currentMutation?.removeObserver(observer2)
+ expect(currentMutation['observers'].length).toEqual(0)
+ expect(queryClient.getMutationCache().getAll()).toHaveLength(1)
+ // wait for cacheTime to gc
+ await sleep(10)
+ await waitFor(() => {
+ expect(queryClient.getMutationCache().getAll()).toHaveLength(0)
+ })
+ })
+
+ test('should be garbage collected later when unsubscribed and mutation is loading', async () => {
+ const queryClient = new QueryClient()
+ const onSuccess = jest.fn()
+ const observer = new MutationObserver(queryClient, {
+ variables: 1,
+ cacheTime: 10,
+ mutationFn: async () => {
+ await sleep(20)
+ return 'data'
+ },
+ onSuccess,
+ })
+ const unsubscribe = observer.subscribe()
+ observer.mutate(1)
+ unsubscribe()
+ expect(queryClient.getMutationCache().getAll()).toHaveLength(1)
+ await sleep(10)
+ // unsubscribe should not remove even though cacheTime has elapsed b/c mutation is still loading
+ expect(queryClient.getMutationCache().getAll()).toHaveLength(1)
+ await sleep(10)
+ // should be removed after an additional cacheTime wait
+ await waitFor(() => {
+ expect(queryClient.getMutationCache().getAll()).toHaveLength(0)
+ })
+ expect(onSuccess).toHaveBeenCalledTimes(1)
+ })
+
+ test('should call callbacks even with cacheTime 0 and mutation still loading', async () => {
+ const queryClient = new QueryClient()
+ const onSuccess = jest.fn()
+ const observer = new MutationObserver(queryClient, {
+ variables: 1,
+ cacheTime: 0,
+ mutationFn: async () => {
+ return 'data'
+ },
+ onSuccess,
+ })
+ const unsubscribe = observer.subscribe()
+ observer.mutate(1)
+ unsubscribe()
+ await waitFor(() => {
+ expect(queryClient.getMutationCache().getAll()).toHaveLength(0)
+ })
+ expect(onSuccess).toHaveBeenCalledTimes(1)
+ })
+ })
})
diff --git a/src/core/tests/mutationObserver.test.tsx b/src/core/tests/mutationObserver.test.tsx
index 68521fb4fa..c1f577f936 100644
--- a/src/core/tests/mutationObserver.test.tsx
+++ b/src/core/tests/mutationObserver.test.tsx
@@ -1,5 +1,5 @@
import { waitFor } from '@testing-library/react'
-import { sleep } from '../../react/tests/utils'
+import { sleep } from '../../reactjs/tests/utils'
import { QueryClient, MutationObserver } from '../..'
describe('mutationObserver', () => {
diff --git a/src/core/tests/mutations.test.tsx b/src/core/tests/mutations.test.tsx
index f606ee6028..14260c38db 100644
--- a/src/core/tests/mutations.test.tsx
+++ b/src/core/tests/mutations.test.tsx
@@ -1,5 +1,10 @@
import { QueryClient } from '../..'
-import { mockConsoleError, queryKey, sleep } from '../../react/tests/utils'
+import {
+ executeMutation,
+ mockConsoleError,
+ queryKey,
+ sleep,
+} from '../../reactjs/tests/utils'
import { MutationState } from '../mutation'
import { MutationObserver } from '../mutationObserver'
@@ -16,7 +21,7 @@ describe('mutations', () => {
})
test('mutate should trigger a mutation', async () => {
- const result = await queryClient.executeMutation({
+ const result = await executeMutation(queryClient, {
mutationFn: async (text: string) => text,
variables: 'todo',
})
@@ -48,7 +53,7 @@ describe('mutations', () => {
mutationFn: async (text: string) => text,
})
- const result = await queryClient.executeMutation({
+ const result = await executeMutation(queryClient, {
mutationKey: key,
variables: 'todo',
})
@@ -345,7 +350,7 @@ describe('mutations', () => {
expect(currentMutation['observers'].length).toEqual(1)
})
- test('executeMutation should throw an error if no mutationFn found', async () => {
+ test('mutate should throw an error if no mutationFn found', async () => {
const consoleMock = mockConsoleError()
const mutation = new MutationObserver(queryClient, {
@@ -371,14 +376,14 @@ describe('mutations', () => {
})
const observer = new MutationObserver(queryClient, {
- mutationKey: 'key',
+ mutationKey: ['key'],
mutationFn,
})
observer.mutate()
const mutation = queryClient
.getMutationCache()
- .find({ mutationKey: 'key' })!
+ .find({ mutationKey: ['key'] })!
await sleep(10)
// Force current mutation retryer to be undefined
@@ -393,7 +398,7 @@ describe('mutations', () => {
test('reducer should return the state for an unknown action type', async () => {
const observer = new MutationObserver(queryClient, {
- mutationKey: 'key',
+ mutationKey: ['key'],
mutationFn: async () => 'data',
})
@@ -402,7 +407,7 @@ describe('mutations', () => {
observer.mutate()
const mutation = queryClient
.getMutationCache()
- .find({ mutationKey: 'key' })!
+ .find({ mutationKey: ['key'] })!
const prevState = observer.getCurrentResult()
spy.mockReset()
diff --git a/src/core/tests/queriesObserver.test.tsx b/src/core/tests/queriesObserver.test.tsx
index f2fcae9846..c4e5739735 100644
--- a/src/core/tests/queriesObserver.test.tsx
+++ b/src/core/tests/queriesObserver.test.tsx
@@ -1,5 +1,5 @@
import { waitFor } from '@testing-library/react'
-import { sleep, queryKey } from '../../react/tests/utils'
+import { sleep, queryKey } from '../../reactjs/tests/utils'
import {
QueryClient,
QueriesObserver,
@@ -55,34 +55,6 @@ describe('queriesObserver', () => {
expect(observerResult).toMatchObject([{ data: 1 }, { data: 2 }])
})
- test('should return same value for multiple falsy query keys', async () => {
- const queryFn1 = jest.fn().mockReturnValue(1)
- const queryFn2 = jest.fn().mockReturnValue(2)
- const observer = new QueriesObserver(queryClient, [
- { queryKey: undefined, queryFn: queryFn1 },
- ])
- const results: QueryObserverResult[][] = []
- results.push(observer.getCurrentResult())
- const unsubscribe = observer.subscribe(result => {
- results.push(result)
- })
- await sleep(1)
- observer.setQueries([
- { queryKey: undefined, queryFn: queryFn1 },
- { queryKey: '', queryFn: queryFn2 },
- ])
- await sleep(1)
- unsubscribe()
- expect(results.length).toBe(4)
- expect(results[0]).toMatchObject([{ status: 'idle', data: undefined }])
- expect(results[1]).toMatchObject([{ status: 'loading', data: undefined }])
- expect(results[2]).toMatchObject([{ status: 'success', data: 1 }])
- expect(results[3]).toMatchObject([
- { status: 'success', data: 1 },
- { status: 'success', data: 1 },
- ])
- })
-
test('should update when a query updates', async () => {
const key1 = queryKey()
const key2 = queryKey()
@@ -146,11 +118,11 @@ describe('queriesObserver', () => {
observer.setQueries([{ queryKey: key2, queryFn: queryFn2 }])
await sleep(1)
const queryCache = queryClient.getQueryCache()
- expect(queryCache.find(key1, { active: true })).toBeUndefined()
- expect(queryCache.find(key2, { active: true })).toBeDefined()
+ expect(queryCache.find(key1, { type: 'active' })).toBeUndefined()
+ expect(queryCache.find(key2, { type: 'active' })).toBeDefined()
unsubscribe()
- expect(queryCache.find(key1, { active: true })).toBeUndefined()
- expect(queryCache.find(key2, { active: true })).toBeUndefined()
+ expect(queryCache.find(key1, { type: 'active' })).toBeUndefined()
+ expect(queryCache.find(key2, { type: 'active' })).toBeUndefined()
expect(results.length).toBe(6)
expect(results[0]).toMatchObject([
{ status: 'idle', data: undefined },
diff --git a/src/core/tests/query.test.tsx b/src/core/tests/query.test.tsx
index efdc4894e6..118ac6662b 100644
--- a/src/core/tests/query.test.tsx
+++ b/src/core/tests/query.test.tsx
@@ -3,7 +3,7 @@ import {
queryKey,
mockVisibilityState,
mockConsoleError,
-} from '../../react/tests/utils'
+} from '../../reactjs/tests/utils'
import {
QueryCache,
QueryClient,
@@ -13,6 +13,7 @@ import {
onlineManager,
QueryFunctionContext,
} from '../..'
+import { waitFor } from '@testing-library/react'
describe('query', () => {
let queryClient: QueryClient
@@ -46,10 +47,8 @@ describe('query', () => {
it('should continue retry after focus regain and resolve all promises', async () => {
const key = queryKey()
- const originalVisibilityState = document.visibilityState
-
// make page unfocused
- mockVisibilityState('hidden')
+ const visibilityMock = mockVisibilityState('hidden')
let count = 0
let result
@@ -83,7 +82,7 @@ describe('query', () => {
expect(result).toBeUndefined()
// Reset visibilityState to original value
- mockVisibilityState(originalVisibilityState)
+ visibilityMock.mockRestore()
window.dispatchEvent(new FocusEvent('focus'))
// There should not be a result yet
@@ -144,10 +143,8 @@ describe('query', () => {
it('should throw a CancelledError when a paused query is cancelled', async () => {
const key = queryKey()
- const originalVisibilityState = document.visibilityState
-
// make page unfocused
- mockVisibilityState('hidden')
+ const visibilityMock = mockVisibilityState('hidden')
let count = 0
let result
@@ -182,7 +179,7 @@ describe('query', () => {
expect(isCancelledError(result)).toBe(true)
// Reset visibilityState to original value
- mockVisibilityState(originalVisibilityState)
+ visibilityMock.mockRestore()
window.dispatchEvent(new FocusEvent('focus'))
})
@@ -190,7 +187,10 @@ describe('query', () => {
const key = queryKey()
const queryFn = jest
- .fn, [QueryFunctionContext]>()
+ .fn<
+ Promise<'data'>,
+ [QueryFunctionContext>]
+ >()
.mockResolvedValue('data')
queryClient.prefetchQuery(key, queryFn)
@@ -201,7 +201,7 @@ describe('query', () => {
const args = queryFn.mock.calls[0]![0]
expect(args).toBeDefined()
expect(args.pageParam).toBeUndefined()
- expect(args.queryKey).toEqual([key])
+ expect(args.queryKey).toEqual(key)
if (typeof AbortSignal === 'function') {
expect(args.signal).toBeInstanceOf(AbortSignal)
} else {
@@ -277,7 +277,10 @@ describe('query', () => {
test('should provide an AbortSignal to the queryFn that provides info about the cancellation state', async () => {
const key = queryKey()
- const queryFn = jest.fn, [QueryFunctionContext]>()
+ const queryFn = jest.fn<
+ Promise,
+ [QueryFunctionContext>]
+ >()
const onAbort = jest.fn()
const abortListener = jest.fn()
let error
@@ -329,43 +332,6 @@ describe('query', () => {
expect(isCancelledError(error)).toBe(true)
})
- test('should call cancel() fn if it was provided and not continue when last observer unsubscribed', async () => {
- const key = queryKey()
-
- const cancel = jest.fn()
-
- queryClient.prefetchQuery(key, async () => {
- const promise = new Promise((resolve, reject) => {
- sleep(100).then(() => resolve('data'))
- cancel.mockImplementation(() => {
- reject(new Error('Cancelled'))
- })
- }) as any
- promise.cancel = cancel
- return promise
- })
-
- await sleep(10)
-
- // Subscribe and unsubscribe to simulate cancellation because the last observer unsubscribed
- const observer = new QueryObserver(queryClient, {
- queryKey: key,
- enabled: false,
- })
- const unsubscribe = observer.subscribe()
- unsubscribe()
-
- await sleep(100)
-
- const query = queryCache.find(key)!
-
- expect(cancel).toHaveBeenCalled()
- expect(query.state).toMatchObject({
- data: undefined,
- status: 'idle',
- })
- })
-
test('should not continue if explicitly cancelled', async () => {
const key = queryKey()
@@ -507,7 +473,6 @@ describe('query', () => {
})
test('queries with cacheTime 0 should be removed immediately after unsubscribing', async () => {
- const consoleMock = mockConsoleError()
const key = queryKey()
let count = 0
const observer = new QueryObserver(queryClient, {
@@ -521,13 +486,12 @@ describe('query', () => {
})
const unsubscribe1 = observer.subscribe()
unsubscribe1()
- await sleep(10)
+ await waitFor(() => expect(queryCache.find(key)).toBeUndefined())
const unsubscribe2 = observer.subscribe()
unsubscribe2()
- await sleep(10)
- expect(count).toBe(2)
- expect(queryCache.find(key)).toBeUndefined()
- consoleMock.mockRestore()
+
+ await waitFor(() => expect(queryCache.find(key)).toBeUndefined())
+ expect(count).toBe(1)
})
test('should be garbage collected when unsubscribed to', async () => {
@@ -541,7 +505,7 @@ describe('query', () => {
const unsubscribe = observer.subscribe()
expect(queryCache.find(key)).toBeDefined()
unsubscribe()
- expect(queryCache.find(key)).toBeUndefined()
+ await waitFor(() => expect(queryCache.find(key)).toBeUndefined())
})
test('should be garbage collected later when unsubscribed and query is fetching', async () => {
@@ -564,7 +528,7 @@ describe('query', () => {
expect(queryCache.find(key)).toBeDefined()
await sleep(10)
// should be removed after an additional staleTime wait
- expect(queryCache.find(key)).toBeUndefined()
+ await waitFor(() => expect(queryCache.find(key)).toBeUndefined())
})
test('should not be garbage collected unless there are no subscribers', async () => {
diff --git a/src/core/tests/queryCache.test.tsx b/src/core/tests/queryCache.test.tsx
index cf26eb3ef7..e9fcba22cc 100644
--- a/src/core/tests/queryCache.test.tsx
+++ b/src/core/tests/queryCache.test.tsx
@@ -1,4 +1,4 @@
-import { sleep, queryKey, mockConsoleError } from '../../react/tests/utils'
+import { sleep, queryKey, mockConsoleError } from '../../reactjs/tests/utils'
import { QueryCache, QueryClient } from '../..'
import { Query } from '.././query'
@@ -23,7 +23,7 @@ describe('queryCache', () => {
queryClient.setQueryData(key, 'foo')
const query = queryCache.find(key)
await sleep(1)
- expect(subscriber).toHaveBeenCalledWith({ query, type: 'queryAdded' })
+ expect(subscriber).toHaveBeenCalledWith({ query, type: 'added' })
unsubscribe()
})
@@ -43,7 +43,7 @@ describe('queryCache', () => {
queryClient.prefetchQuery(key, () => 'data')
const query = queryCache.find(key)
await sleep(100)
- expect(callback).toHaveBeenCalledWith({ query, type: 'queryAdded' })
+ expect(callback).toHaveBeenCalledWith({ query, type: 'added' })
})
test('should notify subscribers when new query with initialData is added', async () => {
@@ -78,6 +78,7 @@ describe('queryCache', () => {
test('should filter correctly', async () => {
const key1 = queryKey()
const key2 = queryKey()
+ const keyFetching = queryKey()
await queryClient.prefetchQuery(key1, () => 'data1')
await queryClient.prefetchQuery(key2, () => 'data2')
await queryClient.prefetchQuery([{ a: 'a', b: 'b' }], () => 'data3')
@@ -89,23 +90,24 @@ describe('queryCache', () => {
const query4 = queryCache.find(['posts', 1])!
expect(queryCache.findAll(key1)).toEqual([query1])
- expect(queryCache.findAll([key1])).toEqual([query1])
+ // wrapping in an extra array doesn't yield the same results anymore since v4 because keys need to be an array
+ expect(queryCache.findAll([key1])).toEqual([])
expect(queryCache.findAll()).toEqual([query1, query2, query3, query4])
expect(queryCache.findAll({})).toEqual([query1, query2, query3, query4])
- expect(queryCache.findAll(key1, { active: false })).toEqual([query1])
- expect(queryCache.findAll(key1, { active: true })).toEqual([])
+ expect(queryCache.findAll(key1, { type: 'inactive' })).toEqual([query1])
+ expect(queryCache.findAll(key1, { type: 'active' })).toEqual([])
expect(queryCache.findAll(key1, { stale: true })).toEqual([])
expect(queryCache.findAll(key1, { stale: false })).toEqual([query1])
- expect(queryCache.findAll(key1, { stale: false, active: true })).toEqual(
- []
- )
expect(
- queryCache.findAll(key1, { stale: false, active: false })
+ queryCache.findAll(key1, { stale: false, type: 'active' })
+ ).toEqual([])
+ expect(
+ queryCache.findAll(key1, { stale: false, type: 'inactive' })
).toEqual([query1])
expect(
queryCache.findAll(key1, {
stale: false,
- active: false,
+ type: 'inactive',
exact: true,
})
).toEqual([query1])
@@ -128,14 +130,34 @@ describe('queryCache', () => {
query3,
])
expect(queryCache.findAll([{ a: 'a' }], { stale: true })).toEqual([])
- expect(queryCache.findAll([{ a: 'a' }], { active: true })).toEqual([])
- expect(queryCache.findAll([{ a: 'a' }], { inactive: true })).toEqual([
+ expect(queryCache.findAll([{ a: 'a' }], { type: 'active' })).toEqual([])
+ expect(queryCache.findAll([{ a: 'a' }], { type: 'inactive' })).toEqual([
query3,
])
expect(
queryCache.findAll({ predicate: query => query === query3 })
).toEqual([query3])
- expect(queryCache.findAll('posts')).toEqual([query4])
+ expect(queryCache.findAll(['posts'])).toEqual([query4])
+
+ expect(queryCache.findAll({ fetchStatus: 'idle' })).toEqual([
+ query1,
+ query2,
+ query3,
+ query4,
+ ])
+ expect(queryCache.findAll(key2, { fetchStatus: undefined })).toEqual([
+ query2,
+ ])
+
+ const promise = queryClient.prefetchQuery(keyFetching, async () => {
+ await sleep(20)
+ return 'dataFetching'
+ })
+ expect(queryCache.findAll({ fetchStatus: 'fetching' })).toEqual([
+ queryCache.find(keyFetching),
+ ])
+ await promise
+ expect(queryCache.findAll({ fetchStatus: 'fetching' })).toEqual([])
})
test('should return all the queries when no filters are defined', async () => {
diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx
index 30f8e2996c..53de7d72ae 100644
--- a/src/core/tests/queryClient.test.tsx
+++ b/src/core/tests/queryClient.test.tsx
@@ -1,11 +1,20 @@
-import { sleep, queryKey, mockConsoleError } from '../../react/tests/utils'
+import { waitFor } from '@testing-library/react'
+import '@testing-library/jest-dom'
+import React from 'react'
+
+import {
+ sleep,
+ queryKey,
+ mockConsoleError,
+ renderWithClient,
+} from '../../reactjs/tests/utils'
import {
+ useQuery,
InfiniteQueryObserver,
QueryCache,
QueryClient,
QueryFunction,
QueryObserver,
- MutationObserver,
} from '../..'
import { focusManager, onlineManager } from '..'
@@ -166,19 +175,6 @@ describe('queryClient', () => {
expect(queryClient.getQueryData(key)).toBe('qux')
})
- test('should use the same query when using similar string or array query keys', () => {
- const key = queryKey()
- queryClient.setQueryData(key, '1')
- expect(queryClient.getQueryData(key)).toBe('1')
- expect(queryClient.getQueryData([key])).toBe('1')
- queryClient.setQueryData([key], '2')
- expect(queryClient.getQueryData(key)).toBe('2')
- expect(queryClient.getQueryData([key])).toBe('2')
- queryClient.setQueryData(key, '1')
- expect(queryClient.getQueryData(key)).toBe('1')
- expect(queryClient.getQueryData([key])).toBe('1')
- })
-
test('should accept an update function', () => {
const key = queryKey()
@@ -219,6 +215,62 @@ describe('queryClient', () => {
expect(queryCache.find(key)!.state.data).toBe(newData)
})
+
+ test('should not call onSuccess callback of active observers', async () => {
+ const key = queryKey()
+ const onSuccess = jest.fn()
+
+ function Page() {
+ const state = useQuery(key, () => 'data', { onSuccess })
+ return (
+