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 67d19f8fc2..ac8897c93b 100644 --- a/docs/src/pages/guides/filters.md +++ b/docs/src/pages/guides/filters.md @@ -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: @@ -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 }) 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 index b10a0db62b..44000c1e98 100644 --- a/docs/src/pages/guides/migrating-to-react-query-4.md +++ b/docs/src/pages/guides/migrating-to-react-query-4.md @@ -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. 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/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 eb1cdc67b4..acc1a79e88 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] // And 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 2d41e17653..8922070075 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/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/QueryClient.md b/docs/src/pages/reference/QueryClient.md index ff2289d58f..63140f8fae 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: @@ -270,7 +270,7 @@ The `invalidateQueries` method can be used to invalidate and refetch single or m - If you **want inactive queries to refetch** as well, use the `refetchTye: 'all'` option ```js -await queryClient.invalidateQueries('posts', { +await queryClient.invalidateQueries(['posts'], { exact, refetchType: 'active', }, { throwOnError, cancelRefetch }) @@ -343,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** @@ -471,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` @@ -479,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']) } ``` @@ -496,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` @@ -504,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/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/useQuery.md b/docs/src/pages/reference/useQuery.md index e7e1ca89b9..b0017d893b 100644 --- a/docs/src/pages/reference/useQuery.md +++ b/docs/src/pages/reference/useQuery.md @@ -66,7 +66,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,8 +74,7 @@ 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. diff --git a/src/core/query.ts b/src/core/query.ts index 52ba42c6d2..7dfabec548 100644 --- a/src/core/query.ts +++ b/src/core/query.ts @@ -5,7 +5,6 @@ import { noop, replaceEqualDeep, timeUntilStale, - ensureQueryKeyArray, } from './utils' import type { InitialDataFunction, @@ -13,7 +12,6 @@ import type { QueryOptions, QueryStatus, QueryFunctionContext, - EnsuredQueryKey, QueryMeta, CancelOptions, SetDataOptions, @@ -66,7 +64,7 @@ export interface FetchContext< fetchFn: () => unknown | Promise fetchOptions?: FetchOptions options: QueryOptions - queryKey: EnsuredQueryKey + queryKey: TQueryKey state: QueryState meta: QueryMeta | undefined } @@ -374,12 +372,11 @@ 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, } @@ -408,7 +405,7 @@ export class Query< const context: FetchContext = { fetchOptions, options: this.options, - queryKey: queryKey, + queryKey: this.queryKey, state: this.state, fetchFn, meta: this.meta, diff --git a/src/core/tests/hydration.test.tsx b/src/core/tests/hydration.test.tsx index cc7ae08a80..29a44f1d26 100644 --- a/src/core/tests/hydration.test.tsx +++ b/src/core/tests/hydration.test.tsx @@ -12,12 +12,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 +29,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 +66,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 +78,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 +92,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 +103,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 +112,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 +155,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 +170,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 +182,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 +203,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 +213,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 +224,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 +238,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 +246,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 +256,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() @@ -278,7 +278,7 @@ describe('dehydration and rehydration', () => { const serverClient = new QueryClient() - serverClient.setMutationDefaults('addTodo', { + serverClient.setMutationDefaults(['addTodo'], { mutationFn: serverAddTodo, onMutate: serverOnMutate, onSuccess: serverOnSuccess, @@ -288,7 +288,7 @@ describe('dehydration and rehydration', () => { serverClient .executeMutation({ - mutationKey: 'addTodo', + mutationKey: ['addTodo'], variables: { text: 'text' }, }) .catch(() => undefined) @@ -316,7 +316,7 @@ describe('dehydration and rehydration', () => { }) const clientOnSuccess = jest.fn() - client.setMutationDefaults('addTodo', { + client.setMutationDefaults(['addTodo'], { mutationFn: clientAddTodo, onMutate: clientOnMutate, onSuccess: clientOnSuccess, @@ -351,14 +351,14 @@ describe('dehydration and rehydration', () => { const queryClient = new QueryClient() - queryClient.setMutationDefaults('addTodo', { + queryClient.setMutationDefaults(['addTodo'], { mutationFn: serverAddTodo, retry: false, }) queryClient .executeMutation({ - mutationKey: 'addTodo', + mutationKey: ['addTodo'], variables: { text: 'text' }, }) .catch(() => undefined) @@ -382,7 +382,7 @@ describe('dehydration and rehydration', () => { const queryClient = new QueryClient() - queryClient.setMutationDefaults('addTodo', { + queryClient.setMutationDefaults(['addTodo'], { mutationFn: serverAddTodo, retry: 1, retryDelay: 20, @@ -390,7 +390,7 @@ describe('dehydration and rehydration', () => { queryClient .executeMutation({ - mutationKey: 'addTodo', + mutationKey: ['addTodo'], variables: { text: 'text' }, }) .catch(() => undefined) diff --git a/src/core/tests/infiniteQueryBehavior.test.tsx b/src/core/tests/infiniteQueryBehavior.test.tsx index 8ab5389936..a3e57e30e4 100644 --- a/src/core/tests/infiniteQueryBehavior.test.tsx +++ b/src/core/tests/infiniteQueryBehavior.test.tsx @@ -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/mutationCache.test.tsx b/src/core/tests/mutationCache.test.tsx index 37e3f3017b..75753d17a5 100644 --- a/src/core/tests/mutationCache.test.tsx +++ b/src/core/tests/mutationCache.test.tsx @@ -67,10 +67,10 @@ describe('mutationCache', () => { }) 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) @@ -92,16 +92,16 @@ describe('mutationCache', () => { mutationFn: () => Promise.resolve(), }) await testClient.executeMutation({ - mutationKey: 'b', + 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]) diff --git a/src/core/tests/mutations.test.tsx b/src/core/tests/mutations.test.tsx index ad189c5189..af2676e286 100644 --- a/src/core/tests/mutations.test.tsx +++ b/src/core/tests/mutations.test.tsx @@ -371,14 +371,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 +393,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 +402,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/query.test.tsx b/src/core/tests/query.test.tsx index 5bd06adf2d..e44c8df7c2 100644 --- a/src/core/tests/query.test.tsx +++ b/src/core/tests/query.test.tsx @@ -190,7 +190,10 @@ describe('query', () => { const key = queryKey() const queryFn = jest - .fn, [QueryFunctionContext]>() + .fn< + Promise<'data'>, + [QueryFunctionContext>] + >() .mockResolvedValue('data') queryClient.prefetchQuery(key, queryFn) @@ -201,7 +204,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 +280,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 diff --git a/src/core/tests/queryCache.test.tsx b/src/core/tests/queryCache.test.tsx index 09fdec6fff..36ae34a7b8 100644 --- a/src/core/tests/queryCache.test.tsx +++ b/src/core/tests/queryCache.test.tsx @@ -89,7 +89,8 @@ 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, { type: 'inactive' })).toEqual([query1]) @@ -135,7 +136,7 @@ describe('queryCache', () => { expect( queryCache.findAll({ predicate: query => query === query3 }) ).toEqual([query3]) - expect(queryCache.findAll('posts')).toEqual([query4]) + expect(queryCache.findAll(['posts'])).toEqual([query4]) }) 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 79c1398b79..fce37aba88 100644 --- a/src/core/tests/queryClient.test.tsx +++ b/src/core/tests/queryClient.test.tsx @@ -176,19 +176,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() @@ -292,7 +279,10 @@ describe('queryClient', () => { queryClient.setQueryData(['key', 1], 1) queryClient.setQueryData(['key', 2], 2) - const result = queryClient.setQueriesData('key', old => old! + 5) + const result = queryClient.setQueriesData( + ['key'], + old => old! + 5 + ) expect(result).toEqual([ [['key', 1], 6], @@ -318,10 +308,10 @@ describe('queryClient', () => { }) test('should not update non existing queries', () => { - const result = queryClient.setQueriesData('key', 'data') + const result = queryClient.setQueriesData(['key'], 'data') expect(result).toEqual([]) - expect(queryClient.getQueryData('key')).toBe(undefined) + expect(queryClient.getQueryData(['key'])).toBe(undefined) }) }) @@ -378,8 +368,8 @@ describe('queryClient', () => { describe('fetchQuery', () => { test('should not type-error with strict query key', async () => { type StrictData = 'data' - type StrictQueryKey = ['strict', string] - const key: StrictQueryKey = ['strict', queryKey()] + type StrictQueryKey = ['strict', ...ReturnType] + const key: StrictQueryKey = ['strict', ...queryKey()] const fetchFn: QueryFunction = () => Promise.resolve('data') @@ -490,8 +480,8 @@ describe('queryClient', () => { describe('fetchInfiniteQuery', () => { test('should not type-error with strict query key', async () => { type StrictData = string - type StrictQueryKey = ['strict', string] - const key: StrictQueryKey = ['strict', queryKey()] + type StrictQueryKey = ['strict', ...ReturnType] + const key: StrictQueryKey = ['strict', ...queryKey()] const data = { pages: ['data'], @@ -532,8 +522,8 @@ describe('queryClient', () => { describe('prefetchInfiniteQuery', () => { test('should not type-error with strict query key', async () => { type StrictData = 'data' - type StrictQueryKey = ['strict', string] - const key: StrictQueryKey = ['strict', queryKey()] + type StrictQueryKey = ['strict', ...ReturnType] + const key: StrictQueryKey = ['strict', ...queryKey()] const fetchFn: QueryFunction = () => Promise.resolve('data') @@ -572,8 +562,8 @@ describe('queryClient', () => { describe('prefetchQuery', () => { test('should not type-error with strict query key', async () => { type StrictData = 'data' - type StrictQueryKey = ['strict', string] - const key: StrictQueryKey = ['strict', queryKey()] + type StrictQueryKey = ['strict', ...ReturnType] + const key: StrictQueryKey = ['strict', ...queryKey()] const fetchFn: QueryFunction = () => Promise.resolve('data') diff --git a/src/core/tests/utils.test.tsx b/src/core/tests/utils.test.tsx index 13fda6b217..2297c2a6b7 100644 --- a/src/core/tests/utils.test.tsx +++ b/src/core/tests/utils.test.tsx @@ -341,14 +341,14 @@ describe('core/utils', () => { describe('parseMutationArgs', () => { it('should return mutation options', () => { - const options = { mutationKey: 'key' } + const options = { mutationKey: ['key'] } expect(parseMutationArgs(options)).toMatchObject(options) }) }) describe('matchMutation', () => { it('should return false if mutationKey options is undefined', () => { - const filters = { mutationKey: 'key1' } + const filters = { mutationKey: ['key1'] } const queryClient = new QueryClient() const mutation = new Mutation({ mutationId: 1, diff --git a/src/core/types.ts b/src/core/types.ts index 3e3817f339..74926e45fe 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -3,11 +3,7 @@ import type { QueryBehavior, Query } from './query' import type { RetryValue, RetryDelayValue } from './retryer' import type { QueryFilters, QueryTypeFilter } from './utils' -export type QueryKey = string | readonly unknown[] -export type EnsuredQueryKey = T extends string - ? [T] - : Exclude - +export type QueryKey = readonly unknown[] export type QueryFunction< T = unknown, TQueryKey extends QueryKey = QueryKey @@ -17,7 +13,7 @@ export interface QueryFunctionContext< TQueryKey extends QueryKey = QueryKey, TPageParam = any > { - queryKey: EnsuredQueryKey + queryKey: TQueryKey signal?: AbortSignal pageParam?: TPageParam meta: QueryMeta | undefined @@ -496,7 +492,7 @@ export type InfiniteQueryObserverResult = | InfiniteQueryObserverRefetchErrorResult | InfiniteQueryObserverSuccessResult -export type MutationKey = string | readonly unknown[] +export type MutationKey = readonly unknown[] export type MutationStatus = 'idle' | 'loading' | 'success' | 'error' diff --git a/src/core/utils.ts b/src/core/utils.ts index 65fbb7e949..578caf7084 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -1,6 +1,5 @@ import type { Mutation } from './mutation' import type { Query } from './query' -import { EnsuredQueryKey } from './types' import type { MutationFunction, MutationKey, @@ -87,14 +86,6 @@ export function isValidTimeout(value: unknown): value is number { return typeof value === 'number' && value >= 0 && value !== Infinity } -export function ensureQueryKeyArray( - value: T -): EnsuredQueryKey { - return (Array.isArray(value) - ? value - : ([value] as unknown)) as EnsuredQueryKey -} - export function difference(array1: T[], array2: T[]): T[] { return array1.filter(x => array2.indexOf(x) === -1) } @@ -254,17 +245,10 @@ export function hashQueryKeyByOptions( /** * Default query keys hash function. - */ -export function hashQueryKey(queryKey: QueryKey): string { - const asArray = ensureQueryKeyArray(queryKey) - return stableValueHash(asArray) -} - -/** * Hashes the value into a stable hash. */ -export function stableValueHash(value: any): string { - return JSON.stringify(value, (_, val) => +export function hashQueryKey(queryKey: QueryKey): string { + return JSON.stringify(queryKey, (_, val) => isPlainObject(val) ? Object.keys(val) .sort() @@ -280,7 +264,7 @@ export function stableValueHash(value: any): string { * Checks if key `b` partially matches with key `a`. */ export function partialMatchKey(a: QueryKey, b: QueryKey): boolean { - return partialDeepEqual(ensureQueryKeyArray(a), ensureQueryKeyArray(b)) + return partialDeepEqual(a, b) } /** @@ -385,8 +369,8 @@ function hasObjectPrototype(o: any): boolean { return Object.prototype.toString.call(o) === '[object Object]' } -export function isQueryKey(value: any): value is QueryKey { - return typeof value === 'string' || Array.isArray(value) +export function isQueryKey(value: unknown): value is QueryKey { + return Array.isArray(value) } export function isError(value: any): value is Error { diff --git a/src/createWebStoragePersister/tests/storageIsFull.test.ts b/src/createWebStoragePersister/tests/storageIsFull.test.ts index 87e9c69e27..d071c5d87d 100644 --- a/src/createWebStoragePersister/tests/storageIsFull.test.ts +++ b/src/createWebStoragePersister/tests/storageIsFull.test.ts @@ -45,11 +45,11 @@ describe('createWebStoragePersister ', () => { storage, }) - await queryClient.prefetchQuery('string', () => Promise.resolve('string')) - await queryClient.prefetchQuery('number', () => Promise.resolve(1)) - await queryClient.prefetchQuery('boolean', () => Promise.resolve(true)) - await queryClient.prefetchQuery('null', () => Promise.resolve(null)) - await queryClient.prefetchQuery('array', () => + await queryClient.prefetchQuery(['string'], () => Promise.resolve('string')) + await queryClient.prefetchQuery(['number'], () => Promise.resolve(1)) + await queryClient.prefetchQuery(['boolean'], () => Promise.resolve(true)) + await queryClient.prefetchQuery(['null'], () => Promise.resolve(null)) + await queryClient.prefetchQuery(['array'], () => Promise.resolve(['string', 0]) ) @@ -76,16 +76,16 @@ describe('createWebStoragePersister ', () => { storage, }) - await queryClient.prefetchQuery('A', () => Promise.resolve('A'.repeat(N))) + await queryClient.prefetchQuery(['A'], () => Promise.resolve('A'.repeat(N))) await sleep(1) - await queryClient.prefetchQuery('B', () => Promise.resolve('B'.repeat(N))) + await queryClient.prefetchQuery(['B'], () => Promise.resolve('B'.repeat(N))) await sleep(1) - await queryClient.prefetchQuery('C', () => Promise.resolve('C'.repeat(N))) + await queryClient.prefetchQuery(['C'], () => Promise.resolve('C'.repeat(N))) await sleep(1) - await queryClient.prefetchQuery('D', () => Promise.resolve('D'.repeat(N))) + await queryClient.prefetchQuery(['D'], () => Promise.resolve('D'.repeat(N))) await sleep(1) - await queryClient.prefetchQuery('E', () => Promise.resolve('E'.repeat(N))) + await queryClient.prefetchQuery(['E'], () => Promise.resolve('E'.repeat(N))) const persistClient = { buster: 'test-limit', @@ -97,14 +97,14 @@ describe('createWebStoragePersister ', () => { const restoredClient = await webStoragePersister.restoreClient() expect(restoredClient?.clientState.queries.length).toEqual(4) expect( - restoredClient?.clientState.queries.find(q => q.queryKey === 'A') + restoredClient?.clientState.queries.find(q => q.queryKey[0] === 'A') ).toBeUndefined() expect( - restoredClient?.clientState.queries.find(q => q.queryKey === 'B') + restoredClient?.clientState.queries.find(q => q.queryKey[0] === 'B') ).not.toBeUndefined() // update query Data - await queryClient.prefetchQuery('A', () => Promise.resolve('a'.repeat(N))) + await queryClient.prefetchQuery(['A'], () => Promise.resolve('a'.repeat(N))) const updatedPersistClient = { buster: 'test-limit', timestamp: Date.now(), @@ -115,10 +115,10 @@ describe('createWebStoragePersister ', () => { const restoredClient2 = await webStoragePersister.restoreClient() expect(restoredClient2?.clientState.queries.length).toEqual(4) expect( - restoredClient2?.clientState.queries.find(q => q.queryKey === 'A') + restoredClient2?.clientState.queries.find(q => q.queryKey[0] === 'A') ).toHaveProperty('state.data', 'a'.repeat(N)) expect( - restoredClient2?.clientState.queries.find(q => q.queryKey === 'B') + restoredClient2?.clientState.queries.find(q => q.queryKey[0] === 'B') ).toBeUndefined() }) @@ -137,7 +137,7 @@ describe('createWebStoragePersister ', () => { mutationCache.build( queryClient, { - mutationKey: 'MUTATIONS', + mutationKey: ['MUTATIONS'], mutationFn: () => Promise.resolve('M'.repeat(N)), }, { @@ -151,12 +151,12 @@ describe('createWebStoragePersister ', () => { } ) await sleep(1) - await queryClient.prefetchQuery('A', () => Promise.resolve('A'.repeat(N))) + await queryClient.prefetchQuery(['A'], () => Promise.resolve('A'.repeat(N))) await sleep(1) - await queryClient.prefetchQuery('B', () => Promise.resolve('B'.repeat(N))) - await queryClient.prefetchQuery('C', () => Promise.resolve('C'.repeat(N))) + await queryClient.prefetchQuery(['B'], () => Promise.resolve('B'.repeat(N))) + await queryClient.prefetchQuery(['C'], () => Promise.resolve('C'.repeat(N))) await sleep(1) - await queryClient.prefetchQuery('D', () => Promise.resolve('D'.repeat(N))) + await queryClient.prefetchQuery(['D'], () => Promise.resolve('D'.repeat(N))) const persistClient = { buster: 'test-limit-mutations', @@ -169,7 +169,7 @@ describe('createWebStoragePersister ', () => { expect(restoredClient?.clientState.mutations.length).toEqual(1) expect(restoredClient?.clientState.queries.length).toEqual(3) expect( - restoredClient?.clientState.queries.find(q => q.queryKey === 'A') + restoredClient?.clientState.queries.find(q => q.queryKey === ['A']) ).toBeUndefined() }) }) diff --git a/src/devtools/tests/devtools.test.tsx b/src/devtools/tests/devtools.test.tsx index b529052a03..fa84f296ee 100644 --- a/src/devtools/tests/devtools.test.tsx +++ b/src/devtools/tests/devtools.test.tsx @@ -20,7 +20,7 @@ describe('ReactQueryDevtools', () => { const { queryClient } = createQueryClient() function Page() { - const { data = 'default' } = useQuery('check', async () => { + const { data = 'default' } = useQuery(['check'], async () => { await sleep(10) return 'test' }) @@ -57,7 +57,7 @@ describe('ReactQueryDevtools', () => { function Page() { const { data = 'default' } = useQuery( - 'check', + ['check'], async () => { await sleep(100) return 'test' @@ -98,7 +98,7 @@ describe('ReactQueryDevtools', () => { screen.getByRole('button', { name: /open react query devtools/i }) ) - const currentQuery = queryCache.find('check') + const currentQuery = queryCache.find(['check']) // When the query is fetching then expect number of // fetching queries to be 1 @@ -140,7 +140,7 @@ describe('ReactQueryDevtools', () => { const { queryClient, queryCache } = createQueryClient() function Page() { - const { data = 'default' } = useQuery('check', async () => { + const { data = 'default' } = useQuery(['check'], async () => { await sleep(10) return 'test' }) @@ -158,7 +158,7 @@ describe('ReactQueryDevtools', () => { screen.getByRole('button', { name: /open react query devtools/i }) ) - const currentQuery = queryCache.find('check') + const currentQuery = queryCache.find(['check']) await screen.findByText(getByTextContent(`1${currentQuery?.queryHash}`)) @@ -175,17 +175,17 @@ describe('ReactQueryDevtools', () => { const { queryClient, queryCache } = createQueryClient() function Page() { - const fooResult = useQuery('foo', async () => { + const fooResult = useQuery(['foo'], async () => { await sleep(10) return 'foo-result' }) - const barResult = useQuery('bar', async () => { + const barResult = useQuery(['bar'], async () => { await sleep(10) return 'bar-result' }) - const bazResult = useQuery('baz', async () => { + const bazResult = useQuery(['baz'], async () => { await sleep(10) return 'baz-result' }) @@ -205,9 +205,9 @@ describe('ReactQueryDevtools', () => { screen.getByRole('button', { name: /open react query devtools/i }) ) - const fooQueryHash = queryCache.find('foo')?.queryHash ?? 'invalid hash' - const barQueryHash = queryCache.find('bar')?.queryHash ?? 'invalid hash' - const bazQueryHash = queryCache.find('baz')?.queryHash ?? 'invalid hash' + const fooQueryHash = queryCache.find(['foo'])?.queryHash ?? 'invalid hash' + const barQueryHash = queryCache.find(['bar'])?.queryHash ?? 'invalid hash' + const bazQueryHash = queryCache.find(['baz'])?.queryHash ?? 'invalid hash' await screen.findByText(fooQueryHash) screen.getByText(barQueryHash) @@ -231,7 +231,7 @@ describe('ReactQueryDevtools', () => { function Page() { const [enabled, setEnabled] = React.useState(false) const { data } = useQuery( - 'key', + ['key'], async () => { await sleep(10) return 'test' @@ -264,7 +264,7 @@ describe('ReactQueryDevtools', () => { const { queryClient } = createQueryClient() function Page() { - const { data } = useQuery('key', () => Promise.resolve('test'), { + const { data } = useQuery(['key'], () => Promise.resolve('test'), { enabled: false, }) @@ -301,18 +301,18 @@ describe('ReactQueryDevtools', () => { const { queryClient, queryCache } = createQueryClient() function Page() { - const query1Result = useQuery('query-1', async () => { + const query1Result = useQuery(['query-1'], async () => { await sleep(20) return 'query-1-result' }) - const query2Result = useQuery('query-2', async () => { + const query2Result = useQuery(['query-2'], async () => { await sleep(60) return 'query-2-result' }) const query3Result = useQuery( - 'query-3', + ['query-3'], async () => { await sleep(40) return 'query-3-result' @@ -335,9 +335,9 @@ describe('ReactQueryDevtools', () => { screen.getByRole('button', { name: /open react query devtools/i }) ) - const query1Hash = queryCache.find('query-1')?.queryHash ?? 'invalid hash' - const query2Hash = queryCache.find('query-2')?.queryHash ?? 'invalid hash' - const query3Hash = queryCache.find('query-3')?.queryHash ?? 'invalid hash' + const query1Hash = queryCache.find(['query-1'])?.queryHash ?? 'invalid hash' + const query2Hash = queryCache.find(['query-2'])?.queryHash ?? 'invalid hash' + const query3Hash = queryCache.find(['query-3'])?.queryHash ?? 'invalid hash' const sortSelect = screen.getByLabelText(/sort queries/i) let queries = [] diff --git a/src/reactjs/tests/Hydrate.test.tsx b/src/reactjs/tests/Hydrate.test.tsx index 3ea92d41c7..12924afa9d 100644 --- a/src/reactjs/tests/Hydrate.test.tsx +++ b/src/reactjs/tests/Hydrate.test.tsx @@ -16,13 +16,13 @@ import * as coreModule from '../../core/index' describe('React hydration', () => { const fetchData: (value: string) => Promise = value => new Promise(res => setTimeout(() => res(value), 10)) - const dataQuery: (key: string) => Promise = key => fetchData(key) + const dataQuery: (key: [string]) => Promise = key => fetchData(key[0]) let stringifiedState: string beforeAll(async () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) - await queryClient.prefetchQuery('string', () => dataQuery('string')) + await queryClient.prefetchQuery(['string'], () => dataQuery(['string'])) const dehydrated = dehydrate(queryClient) stringifiedState = JSON.stringify(dehydrated) queryClient.clear() @@ -36,7 +36,7 @@ describe('React hydration', () => { function Page() { useHydrate(dehydratedState) - const { data } = useQuery('string', () => dataQuery('string')) + const { data } = useQuery(['string'], () => dataQuery(['string'])) return (

{data}

@@ -62,7 +62,7 @@ describe('React hydration', () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) - function Page({ queryKey }: { queryKey: string }) { + function Page({ queryKey }: { queryKey: [string] }) { const { data } = useQuery(queryKey, () => dataQuery(queryKey)) return (
@@ -74,7 +74,7 @@ describe('React hydration', () => { const rendered = render( - + ) @@ -86,11 +86,11 @@ describe('React hydration', () => { const intermediateClient = new QueryClient({ queryCache: intermediateCache, }) - await intermediateClient.prefetchQuery('string', () => - dataQuery('should change') + await intermediateClient.prefetchQuery(['string'], () => + dataQuery(['should change']) ) - await intermediateClient.prefetchQuery('added string', () => - dataQuery('added string') + await intermediateClient.prefetchQuery(['added string'], () => + dataQuery(['added string']) ) const dehydrated = dehydrate(intermediateClient) intermediateClient.clear() @@ -98,8 +98,8 @@ describe('React hydration', () => { rendered.rerender( - - + + ) @@ -120,7 +120,7 @@ describe('React hydration', () => { const queryClient = new QueryClient({ queryCache }) function Page() { - const { data } = useQuery('string', () => dataQuery('string')) + const { data } = useQuery(['string'], () => dataQuery(['string'])) return (

{data}

diff --git a/src/reactjs/tests/ssr-hydration.test.tsx b/src/reactjs/tests/ssr-hydration.test.tsx index f855b86352..d165543f28 100644 --- a/src/reactjs/tests/ssr-hydration.test.tsx +++ b/src/reactjs/tests/ssr-hydration.test.tsx @@ -36,7 +36,7 @@ describe('Server side rendering with de/rehydration', () => { // -- Shared part -- function SuccessComponent() { - const result = useQuery('success', () => fetchDataSuccess('success!')) + const result = useQuery(['success'], () => fetchDataSuccess('success!')) return ( ) @@ -47,7 +47,7 @@ describe('Server side rendering with de/rehydration', () => { const prefetchCache = new QueryCache() const prefetchClient = new QueryClient({ queryCache: prefetchCache }) - await prefetchClient.prefetchQuery('success', () => + await prefetchClient.prefetchQuery(['success'], () => fetchDataSuccess('success') ) const dehydratedStateServer = dehydrate(prefetchClient) @@ -101,7 +101,9 @@ describe('Server side rendering with de/rehydration', () => { // -- Shared part -- function ErrorComponent() { - const result = useQuery('error', () => fetchDataError(), { retry: false }) + const result = useQuery(['error'], () => fetchDataError(), { + retry: false, + }) return ( ) @@ -111,7 +113,7 @@ describe('Server side rendering with de/rehydration', () => { setIsServer(true) const prefetchCache = new QueryCache() const prefetchClient = new QueryClient({ queryCache: prefetchCache }) - await prefetchClient.prefetchQuery('error', () => fetchDataError()) + await prefetchClient.prefetchQuery(['error'], () => fetchDataError()) const dehydratedStateServer = dehydrate(prefetchClient) const renderCache = new QueryCache() const renderClient = new QueryClient({ queryCache: renderCache }) @@ -166,7 +168,7 @@ describe('Server side rendering with de/rehydration', () => { // -- Shared part -- function SuccessComponent() { - const result = useQuery('success', () => fetchDataSuccess('success!')) + const result = useQuery(['success'], () => fetchDataSuccess('success!')) return ( ) diff --git a/src/reactjs/tests/suspense.test.tsx b/src/reactjs/tests/suspense.test.tsx index 50a63e22d4..45e35d70d6 100644 --- a/src/reactjs/tests/suspense.test.tsx +++ b/src/reactjs/tests/suspense.test.tsx @@ -74,7 +74,7 @@ describe("useQuery's in Suspense mode", () => { function Page() { const [multiplier, setMultiplier] = React.useState(1) const state = useInfiniteQuery( - `${key}_${multiplier}`, + [`${key}_${multiplier}`], ({ pageParam = 1 }) => Number(pageParam * multiplier), { suspense: true, @@ -439,7 +439,7 @@ describe("useQuery's in Suspense mode", () => { const key1 = queryKey() const key2 = queryKey() - function Component(props: { queryKey: string }) { + function Component(props: { queryKey: Array }) { const result = useQuery( props.queryKey, async () => { @@ -766,7 +766,7 @@ describe("useQuery's in Suspense mode", () => { function Page() { const [nonce] = React.useState(0) - const queryKeys = `${key}-${succeed}` + const queryKeys = [`${key}-${succeed}`] const result = useQuery( queryKeys, async () => { diff --git a/src/reactjs/tests/useInfiniteQuery.test.tsx b/src/reactjs/tests/useInfiniteQuery.test.tsx index 540b44956d..37cb33449a 100644 --- a/src/reactjs/tests/useInfiniteQuery.test.tsx +++ b/src/reactjs/tests/useInfiniteQuery.test.tsx @@ -755,7 +755,7 @@ describe('useInfiniteQuery', () => { const abortListeners: jest.Mock[] = [] const fetchPage = jest.fn< Promise, - [QueryFunctionContext] + [QueryFunctionContext] >(async ({ pageParam = start, signal }) => { if (signal) { const onAbort = jest.fn() @@ -798,7 +798,7 @@ describe('useInfiniteQuery', () => { let callIndex = 0 const firstCtx = fetchPage.mock.calls[callIndex]![0] expect(firstCtx.pageParam).toBeUndefined() - expect(firstCtx.queryKey).toEqual([key]) + expect(firstCtx.queryKey).toEqual(key) if (typeof AbortSignal === 'function') { expect(firstCtx.signal).toBeInstanceOf(AbortSignal) expect(firstCtx.signal?.aborted).toBe(false) @@ -809,7 +809,7 @@ describe('useInfiniteQuery', () => { callIndex = 1 const secondCtx = fetchPage.mock.calls[callIndex]![0] expect(secondCtx.pageParam).toBe(11) - expect(secondCtx.queryKey).toEqual([key]) + expect(secondCtx.queryKey).toEqual(key) if (typeof AbortSignal === 'function') { expect(secondCtx.signal).toBeInstanceOf(AbortSignal) expect(secondCtx.signal?.aborted).toBe(true) @@ -820,7 +820,7 @@ describe('useInfiniteQuery', () => { callIndex = 2 const thirdCtx = fetchPage.mock.calls[callIndex]![0] expect(thirdCtx.pageParam).toBe(11) - expect(thirdCtx.queryKey).toEqual([key]) + expect(thirdCtx.queryKey).toEqual(key) if (typeof AbortSignal === 'function') { expect(thirdCtx.signal).toBeInstanceOf(AbortSignal) expect(thirdCtx.signal?.aborted).toBe(false) @@ -836,7 +836,7 @@ describe('useInfiniteQuery', () => { const abortListeners: jest.Mock[] = [] const fetchPage = jest.fn< Promise, - [QueryFunctionContext] + [QueryFunctionContext] >(async ({ pageParam = start, signal }) => { if (signal) { const onAbort = jest.fn() @@ -879,7 +879,7 @@ describe('useInfiniteQuery', () => { let callIndex = 0 const firstCtx = fetchPage.mock.calls[callIndex]![0] expect(firstCtx.pageParam).toBeUndefined() - expect(firstCtx.queryKey).toEqual([key]) + expect(firstCtx.queryKey).toEqual(key) if (typeof AbortSignal === 'function') { expect(firstCtx.signal).toBeInstanceOf(AbortSignal) expect(firstCtx.signal?.aborted).toBe(false) @@ -890,7 +890,7 @@ describe('useInfiniteQuery', () => { callIndex = 1 const secondCtx = fetchPage.mock.calls[callIndex]![0] expect(secondCtx.pageParam).toBe(11) - expect(secondCtx.queryKey).toEqual([key]) + expect(secondCtx.queryKey).toEqual(key) if (typeof AbortSignal === 'function') { expect(secondCtx.signal).toBeInstanceOf(AbortSignal) expect(secondCtx.signal?.aborted).toBe(false) diff --git a/src/reactjs/tests/useIsMutating.test.tsx b/src/reactjs/tests/useIsMutating.test.tsx index 10f56cdee3..33af902593 100644 --- a/src/reactjs/tests/useIsMutating.test.tsx +++ b/src/reactjs/tests/useIsMutating.test.tsx @@ -18,11 +18,11 @@ describe('useIsMutating', () => { } function Page() { - const { mutate: mutate1 } = useMutation('mutation1', async () => { + const { mutate: mutate1 } = useMutation(['mutation1'], async () => { await sleep(150) return 'data' }) - const { mutate: mutate2 } = useMutation('mutation2', async () => { + const { mutate: mutate2 } = useMutation(['mutation2'], async () => { await sleep(50) return 'data' }) @@ -46,17 +46,17 @@ describe('useIsMutating', () => { const queryClient = new QueryClient() function IsMutating() { - const isMutating = useIsMutating('mutation1') + const isMutating = useIsMutating(['mutation1']) isMutatings.push(isMutating) return null } function Page() { - const { mutate: mutate1 } = useMutation('mutation1', async () => { + const { mutate: mutate1 } = useMutation(['mutation1'], async () => { await sleep(100) return 'data' }) - const { mutate: mutate2 } = useMutation('mutation2', async () => { + const { mutate: mutate2 } = useMutation(['mutation2'], async () => { await sleep(100) return 'data' }) @@ -79,18 +79,19 @@ describe('useIsMutating', () => { function IsMutating() { const isMutating = useIsMutating({ - predicate: mutation => mutation.options.mutationKey === 'mutation1', + predicate: mutation => + mutation.options.mutationKey?.[0] === 'mutation1', }) isMutatings.push(isMutating) return null } function Page() { - const { mutate: mutate1 } = useMutation('mutation1', async () => { + const { mutate: mutate1 } = useMutation(['mutation1'], async () => { await sleep(100) return 'data' }) - const { mutate: mutate2 } = useMutation('mutation2', async () => { + const { mutate: mutate2 } = useMutation(['mutation2'], async () => { await sleep(100) return 'data' }) @@ -132,7 +133,7 @@ describe('useIsMutating', () => { function Page() { const [mounted, setMounted] = React.useState(true) - const { mutate: mutate1 } = useMutation('mutation1', async () => { + const { mutate: mutate1 } = useMutation(['mutation1'], async () => { await sleep(10) return 'data' }) diff --git a/src/reactjs/tests/useQueries.test.tsx b/src/reactjs/tests/useQueries.test.tsx index 423184b9aa..d1584f7101 100644 --- a/src/reactjs/tests/useQueries.test.tsx +++ b/src/reactjs/tests/useQueries.test.tsx @@ -685,11 +685,11 @@ describe('useQueries', () => { // Array as const does not throw error const result5 = useQueries([ { - queryKey: 'key1', + queryKey: ['key1'], queryFn: () => 'string', }, { - queryKey: 'key1', + queryKey: ['key1'], queryFn: () => 123, }, ] as const) diff --git a/src/reactjs/tests/useQuery.test.tsx b/src/reactjs/tests/useQuery.test.tsx index c0a9a77584..b8b6a3b704 100644 --- a/src/reactjs/tests/useQuery.test.tsx +++ b/src/reactjs/tests/useQuery.test.tsx @@ -91,13 +91,16 @@ describe('useQuery', () => { queryFn: getMyDataArrayKey, }) - const getMyDataStringKey: QueryFunction = async context => { + const getMyDataStringKey: QueryFunction< + MyData, + ['1'] + > = async context => { expectType<['1']>(context.queryKey) return Number(context.queryKey[0]) + 42 } useQuery({ - queryKey: '1', + queryKey: ['1'], queryFn: getMyDataStringKey, }) } @@ -2426,10 +2429,10 @@ describe('useQuery', () => { it('should not pass stringified variables to query function', async () => { const key = queryKey() const variables = { number: 5, boolean: false, object: {}, array: [] } - type QueryKey = [string, typeof variables] - const states: UseQueryResult[] = [] + type CustomQueryKey = [typeof key, typeof variables] + const states: UseQueryResult[] = [] - const queryFn = async (ctx: QueryFunctionContext) => { + const queryFn = async (ctx: QueryFunctionContext) => { await sleep(10) return ctx.queryKey } @@ -3715,7 +3718,7 @@ describe('useQuery', () => { it('should accept an empty string as query key', async () => { function Page() { - const result = useQuery('', ctx => ctx.queryKey) + const result = useQuery([''], ctx => ctx.queryKey) return <>{JSON.stringify(result.data)} } @@ -3988,7 +3991,7 @@ describe('useQuery', () => { const key = queryKey() const states: UseQueryResult[] = [] - const queryFn: QueryFunction = async ctx => { + const queryFn: QueryFunction = async ctx => { const [, limit] = ctx.queryKey const value = limit % 2 && ctx.signal ? 'abort' : `data ${limit}` await sleep(10) diff --git a/src/reactjs/tests/utils.tsx b/src/reactjs/tests/utils.tsx index 314c3183be..b594a1f117 100644 --- a/src/reactjs/tests/utils.tsx +++ b/src/reactjs/tests/utils.tsx @@ -37,9 +37,9 @@ export function mockConsoleError() { } let queryKeyCount = 0 -export function queryKey(): string { +export function queryKey(): Array { queryKeyCount++ - return `query_${queryKeyCount}` + return [`query_${queryKeyCount}`] } export function sleep(timeout: number): Promise {