Skip to content

Commit

Permalink
2919 query key array (#2988)
Browse files Browse the repository at this point in the history
* feat: query key array

remove code that internally ensures that we get an Array, because it is now the expected interface, ensured by TypeScript

* feat: query key array

update tests to the new syntax

* feat: query key array

fix assertions, because there is no array wrapping happening internally anymore. The key you receive from the context is exactly the key you passed in

* feat: query key array

this test doesn't make much sense anymore

* feat: query key array

 wrapping in an extra array doesn't yield the same results anymore since v4 because keys need to be an array

* feat: query key array

make docs adhere to new array key syntax

* feat: query key array

migration docs
  • Loading branch information
TkDodo committed Nov 19, 2021
1 parent a090fe5 commit 2624c42
Show file tree
Hide file tree
Showing 51 changed files with 331 additions and 333 deletions.
14 changes: 7 additions & 7 deletions docs/src/pages/guides/caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
5 changes: 2 additions & 3 deletions docs/src/pages/guides/default-query-function.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
})

Expand Down
2 changes: 1 addition & 1 deletion docs/src/pages/guides/disabling-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function Todos() {
error,
refetch,
isFetching,
} = useQuery('todos', fetchTodoList, {
} = useQuery(['todos'], fetchTodoList, {
enabled: false,
})

Expand Down
6 changes: 3 additions & 3 deletions docs/src/pages/guides/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ 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', { type: 'inactive' })
queryClient.removeQueries(['posts'], { type: 'inactive' })

// Refetch all active queries
await queryClient.refetchQueries({ type: 'active' })

// Refetch all active queries that begin with `posts` in the key
await queryClient.refetchQueries('posts', { type: 'active' })
await queryClient.refetchQueries(['posts'], { type: 'active' })
```

A query filter object supports the following properties:
Expand Down Expand Up @@ -51,7 +51,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 })
Expand Down
14 changes: 7 additions & 7 deletions docs/src/pages/guides/infinite-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function Projects() {
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery('projects', fetchProjects, {
} = useInfiniteQuery(['projects'], fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})

Expand Down Expand Up @@ -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,
})

Expand Down Expand Up @@ -132,7 +132,7 @@ function Projects() {
isFetchingNextPage,
fetchNextPage,
hasNextPage,
} = useInfiniteQuery('projects', fetchProjects, {
} = useInfiniteQuery(['projects'], fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})

Expand All @@ -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,
})
Expand All @@ -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(),
Expand All @@ -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),
}))
Expand All @@ -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,
}))
Expand Down
18 changes: 9 additions & 9 deletions docs/src/pages/guides/initial-query-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
}
Expand All @@ -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,
})
}
Expand All @@ -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,
})
Expand All @@ -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
Expand All @@ -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()
},
Expand All @@ -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)
},
})
}
Expand All @@ -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,
})
}
```
Expand All @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions docs/src/pages/guides/invalidations-from-mutations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
},
})
```
Expand Down
16 changes: 8 additions & 8 deletions docs/src/pages/guides/migrating-to-react-query-3.md
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}))
Expand Down Expand Up @@ -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 <div>Username: {data.username}</div>
Expand All @@ -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 <div>Username: {data.username}</div>
Expand Down Expand Up @@ -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 <div>Username: {data}</div>
Expand Down Expand Up @@ -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'])
}
```

Expand All @@ -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'])
}
```

Expand Down
13 changes: 13 additions & 0 deletions docs/src/pages/guides/migrating-to-react-query-4.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ 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.
Expand Down
12 changes: 6 additions & 6 deletions docs/src/pages/guides/mutations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions docs/src/pages/guides/optimistic-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
},
})
```
Expand Down
6 changes: 3 additions & 3 deletions docs/src/pages/guides/parallel-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
...
}
```
Expand Down

0 comments on commit 2624c42

Please sign in to comment.