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 14002bec89c..d8ee9beb50a 100644 --- a/docs/src/pages/guides/migrating-to-react-query-4.md +++ b/docs/src/pages/guides/migrating-to-react-query-4.md @@ -207,6 +207,16 @@ new QueryClient({ }) ``` +### new API for `useQueries` + +The `useQueries` hook now accepts an object with a `queries` prop as its input. The value of the `queries` prop is an array of queries (this array is identical to what was passed into `useQueries` in v3). + +```diff +- useQueries([{ queryKey1, queryFn1, options1 }, { queryKey2, queryFn2, options2 }]) ++ useQueries({ queries: [{ queryKey1, queryFn1, options1 }, { queryKey2, queryFn2, options2 }] }) +``` + + ### Removed undocumented methods from the `queryClient` The methods `cancelMutatations` and `executeMutation` were undocumented and unused internally, so we removed them. Since they were just wrappers around methods available on the `mutationCache`, you can still use the functionality. diff --git a/docs/src/pages/guides/parallel-queries.md b/docs/src/pages/guides/parallel-queries.md index 79dac5ce82a..74b20a7015b 100644 --- a/docs/src/pages/guides/parallel-queries.md +++ b/docs/src/pages/guides/parallel-queries.md @@ -25,17 +25,17 @@ function App () { If the number of queries you need to execute is changing from render to render, you cannot use manual querying since that would violate the rules of hooks. Instead, React Query provides a `useQueries` hook, which you can use to dynamically execute as many queries in parallel as you'd like. -`useQueries` accepts an **array of query options objects** and returns an **array of query results**: +`useQueries` accepts an **options object** with a **queries key** whose value is an **array of query objects**. It returns an **array of query results**: ```js function App({ users }) { - const userQueries = useQueries( - users.map(user => { + const userQueries = useQueries({ + queries: users.map(user => { return { queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id), } }) - ) + }) } ``` diff --git a/docs/src/pages/reference/useQueries.md b/docs/src/pages/reference/useQueries.md index f69e5fe3af6..0849033ef20 100644 --- a/docs/src/pages/reference/useQueries.md +++ b/docs/src/pages/reference/useQueries.md @@ -6,15 +6,17 @@ title: useQueries The `useQueries` hook can be used to fetch a variable number of queries: ```js -const results = useQueries([ - { queryKey: ['post', 1], queryFn: fetchPost }, - { queryKey: ['post', 2], queryFn: fetchPost }, -]) +const results = useQueries({ + queries: [ + { queryKey: ['post', 1], queryFn: fetchPost }, + { queryKey: ['post', 2], queryFn: fetchPost } + ] +}) ``` **Options** -The `useQueries` hook accepts an array with query option objects identical to the [`useQuery` hook](/reference/useQuery). +The `useQueries` hook accepts an options object with a **queries** key whose value is an array with query option objects identical to the [`useQuery` hook](/reference/useQuery). **Returns** diff --git a/src/reactjs/tests/useQueries.test.tsx b/src/reactjs/tests/useQueries.test.tsx index 0b549747dc4..f6d260cbbef 100644 --- a/src/reactjs/tests/useQueries.test.tsx +++ b/src/reactjs/tests/useQueries.test.tsx @@ -30,22 +30,24 @@ describe('useQueries', () => { const results: UseQueryResult[][] = [] function Page() { - const result = useQueries([ - { - queryKey: key1, - queryFn: async () => { - await sleep(5) - return 1 + const result = useQueries({ + queries: [ + { + queryKey: key1, + queryFn: async () => { + await sleep(5) + return 1 + }, }, - }, - { - queryKey: key2, - queryFn: async () => { - await sleep(10) - return 2 + { + queryKey: key2, + queryFn: async () => { + await sleep(10) + return 2 + }, }, - }, - ]) + ], + }) results.push(result) return null } @@ -67,24 +69,26 @@ describe('useQueries', () => { function Page() { const [count, setCount] = React.useState(1) - const result = useQueries([ - { - queryKey: [key1, count], - keepPreviousData: true, - queryFn: async () => { - await sleep(5) - return count * 2 + const result = useQueries({ + queries: [ + { + queryKey: [key1, count], + keepPreviousData: true, + queryFn: async () => { + await sleep(5) + return count * 2 + }, }, - }, - { - queryKey: [key2, count], - keepPreviousData: true, - queryFn: async () => { - await sleep(10) - return count * 5 + { + queryKey: [key2, count], + keepPreviousData: true, + queryFn: async () => { + await sleep(10) + return count * 5 + }, }, - }, - ]) + ], + }) states.push(result) React.useEffect(() => { @@ -151,16 +155,16 @@ describe('useQueries', () => { function Page() { const [count, setCount] = React.useState(2) - const result = useQueries( - Array.from({ length: count }, (_, i) => ({ + const result = useQueries({ + queries: Array.from({ length: count }, (_, i) => ({ queryKey: [key, count, i + 1], keepPreviousData: true, queryFn: async () => { await sleep(5 * (i + 1)) return (i + 1) * count * 2 }, - })) - ) + })), + }) states.push(result) @@ -261,8 +265,8 @@ describe('useQueries', () => { const [series2, setSeries2] = React.useState(2) const ids = [series1, series2] - const result = useQueries( - ids.map(id => { + const result = useQueries({ + queries: ids.map(id => { return { queryKey: [key, id], queryFn: async () => { @@ -271,8 +275,8 @@ describe('useQueries', () => { }, keepPreviousData: true, } - }) - ) + }), + }) states.push(result) @@ -356,8 +360,8 @@ describe('useQueries', () => { const [enableId1, setEnableId1] = React.useState(true) const ids = enableId1 ? [1, 2] : [2] - const result = useQueries( - ids.map(id => { + const result = useQueries({ + queries: ids.map(id => { return { queryKey: [key, id], queryFn: async () => { @@ -366,8 +370,8 @@ describe('useQueries', () => { }, keepPreviousData: true, } - }) - ) + }), + }) states.push(result) @@ -443,20 +447,22 @@ describe('useQueries', () => { // @ts-expect-error (Page component is not rendered) // eslint-disable-next-line function Page() { - const result1 = useQueries<[[number], [string], [string[], boolean]]>([ - { - queryKey: key1, - queryFn: () => 1, - }, - { - queryKey: key2, - queryFn: () => 'string', - }, - { - queryKey: key3, - queryFn: () => ['string[]'], - }, - ]) + const result1 = useQueries<[[number], [string], [string[], boolean]]>({ + queries: [ + { + queryKey: key1, + queryFn: () => 1, + }, + { + queryKey: key2, + queryFn: () => 'string', + }, + { + queryKey: key3, + queryFn: () => ['string[]'], + }, + ], + }) expectType>(result1[0]) expectType>(result1[1]) expectType>(result1[2]) @@ -468,80 +474,86 @@ describe('useQueries', () => { // TData (3rd element) takes precedence over TQueryFnData (1st element) const result2 = useQueries< [[string, unknown, string], [string, unknown, number]] - >([ - { - queryKey: key1, - queryFn: () => 'string', - select: a => { - expectType(a) - expectTypeNotAny(a) - return a.toLowerCase() - }, - }, - { - queryKey: key2, - queryFn: () => 'string', - select: a => { - expectType(a) - expectTypeNotAny(a) - return parseInt(a) - }, - }, - ]) + >({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + select: a => { + expectType(a) + expectTypeNotAny(a) + return a.toLowerCase() + }, + }, + { + queryKey: key2, + queryFn: () => 'string', + select: a => { + expectType(a) + expectTypeNotAny(a) + return parseInt(a) + }, + }, + ], + }) expectType>(result2[0]) expectType>(result2[1]) expectType(result2[0].data) expectType(result2[1].data) // types should be enforced - useQueries<[[string, unknown, string], [string, boolean, number]]>([ - { - queryKey: key1, - queryFn: () => 'string', - select: a => { - expectType(a) - expectTypeNotAny(a) - return a.toLowerCase() - }, - onSuccess: a => { - expectType(a) - expectTypeNotAny(a) - }, - placeholderData: 'string', - // @ts-expect-error (initialData: string) - initialData: 123, - }, - { - queryKey: key2, - queryFn: () => 'string', - select: a => { - expectType(a) - expectTypeNotAny(a) - return parseInt(a) - }, - onSuccess: a => { - expectType(a) - expectTypeNotAny(a) - }, - onError: e => { - expectType(e) - expectTypeNotAny(e) - }, - placeholderData: 'string', - // @ts-expect-error (initialData: string) - initialData: 123, - }, - ]) + useQueries<[[string, unknown, string], [string, boolean, number]]>({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + select: a => { + expectType(a) + expectTypeNotAny(a) + return a.toLowerCase() + }, + onSuccess: a => { + expectType(a) + expectTypeNotAny(a) + }, + placeholderData: 'string', + // @ts-expect-error (initialData: string) + initialData: 123, + }, + { + queryKey: key2, + queryFn: () => 'string', + select: a => { + expectType(a) + expectTypeNotAny(a) + return parseInt(a) + }, + onSuccess: a => { + expectType(a) + expectTypeNotAny(a) + }, + onError: e => { + expectType(e) + expectTypeNotAny(e) + }, + placeholderData: 'string', + // @ts-expect-error (initialData: string) + initialData: 123, + }, + ], + }) // field names should be enforced - useQueries<[[string]]>([ - { - queryKey: key1, - queryFn: () => 'string', - // @ts-expect-error (invalidField) - someInvalidField: [], - }, - ]) + useQueries<[[string]]>({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + // @ts-expect-error (invalidField) + someInvalidField: [], + }, + ], + }) } }) @@ -559,20 +571,22 @@ describe('useQueries', () => { { queryFnData: string }, { queryFnData: string[]; error: boolean } ] - >([ - { - queryKey: key1, - queryFn: () => 1, - }, - { - queryKey: key2, - queryFn: () => 'string', - }, - { - queryKey: key3, - queryFn: () => ['string[]'], - }, - ]) + >({ + queries: [ + { + queryKey: key1, + queryFn: () => 1, + }, + { + queryKey: key2, + queryFn: () => 'string', + }, + { + queryKey: key3, + queryFn: () => ['string[]'], + }, + ], + }) expectType>(result1[0]) expectType>(result1[1]) expectType>(result1[2]) @@ -587,52 +601,56 @@ describe('useQueries', () => { { queryFnData: string; data: string }, { queryFnData: string; data: number } ] - >([ - { - queryKey: key1, - queryFn: () => 'string', - select: a => { - expectType(a) - expectTypeNotAny(a) - return a.toLowerCase() - }, - }, - { - queryKey: key2, - queryFn: () => 'string', - select: a => { - expectType(a) - expectTypeNotAny(a) - return parseInt(a) - }, - }, - ]) + >({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + select: a => { + expectType(a) + expectTypeNotAny(a) + return a.toLowerCase() + }, + }, + { + queryKey: key2, + queryFn: () => 'string', + select: a => { + expectType(a) + expectTypeNotAny(a) + return parseInt(a) + }, + }, + ], + }) expectType>(result2[0]) expectType>(result2[1]) expectType(result2[0].data) expectType(result2[1].data) // can pass only TData (data prop) although TQueryFnData will be left unknown - const result3 = useQueries<[{ data: string }, { data: number }]>([ - { - queryKey: key1, - queryFn: () => 'string', - select: a => { - expectType(a) - expectTypeNotAny(a) - return a as string - }, - }, - { - queryKey: key2, - queryFn: () => 'string', - select: a => { - expectType(a) - expectTypeNotAny(a) - return a as number - }, - }, - ]) + const result3 = useQueries<[{ data: string }, { data: number }]>({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + select: a => { + expectType(a) + expectTypeNotAny(a) + return a as string + }, + }, + { + queryKey: key2, + queryFn: () => 'string', + select: a => { + expectType(a) + expectTypeNotAny(a) + return a as number + }, + }, + ], + }) expectType>(result3[0]) expectType>(result3[1]) expectType(result3[0].data) @@ -644,54 +662,58 @@ describe('useQueries', () => { { queryFnData: string; data: string }, { queryFnData: string; data: number; error: boolean } ] - >([ - { - queryKey: key1, - queryFn: () => 'string', - select: a => { - expectType(a) - expectTypeNotAny(a) - return a.toLowerCase() - }, - onSuccess: a => { - expectType(a) - expectTypeNotAny(a) - }, - placeholderData: 'string', - // @ts-expect-error (initialData: string) - initialData: 123, - }, - { - queryKey: key2, - queryFn: () => 'string', - select: a => { - expectType(a) - expectTypeNotAny(a) - return parseInt(a) - }, - onSuccess: a => { - expectType(a) - expectTypeNotAny(a) - }, - onError: e => { - expectType(e) - expectTypeNotAny(e) - }, - placeholderData: 'string', - // @ts-expect-error (initialData: string) - initialData: 123, - }, - ]) + >({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + select: a => { + expectType(a) + expectTypeNotAny(a) + return a.toLowerCase() + }, + onSuccess: a => { + expectType(a) + expectTypeNotAny(a) + }, + placeholderData: 'string', + // @ts-expect-error (initialData: string) + initialData: 123, + }, + { + queryKey: key2, + queryFn: () => 'string', + select: a => { + expectType(a) + expectTypeNotAny(a) + return parseInt(a) + }, + onSuccess: a => { + expectType(a) + expectTypeNotAny(a) + }, + onError: e => { + expectType(e) + expectTypeNotAny(e) + }, + placeholderData: 'string', + // @ts-expect-error (initialData: string) + initialData: 123, + }, + ], + }) // field names should be enforced - useQueries<[{ queryFnData: string }]>([ - { - queryKey: key1, - queryFn: () => 'string', - // @ts-expect-error (invalidField) - someInvalidField: [], - }, - ]) + useQueries<[{ queryFnData: string }]>({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + // @ts-expect-error (invalidField) + someInvalidField: [], + }, + ], + }) } }) @@ -705,40 +727,42 @@ describe('useQueries', () => { // eslint-disable-next-line function Page() { // Array.map preserves TQueryFnData - const result1 = useQueries( - Array(50).map((_, i) => ({ + const result1 = useQueries({ + queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, - })) - ) + })), + }) expectType[]>(result1) expectType(result1[0]?.data) // Array.map preserves TData - const result2 = useQueries( - Array(50).map((_, i) => ({ + const result2 = useQueries({ + queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), - })) - ) + })), + }) expectType[]>(result2) - const result3 = useQueries([ - { - queryKey: key1, - queryFn: () => 1, - }, - { - queryKey: key2, - queryFn: () => 'string', - }, - { - queryKey: key3, - queryFn: () => ['string[]'], - select: () => 123, - }, - ]) + const result3 = useQueries({ + queries: [ + { + queryKey: key1, + queryFn: () => 1, + }, + { + queryKey: key2, + queryFn: () => 'string', + }, + { + queryKey: key3, + queryFn: () => ['string[]'], + select: () => 123, + }, + ], + }) expectType>(result3[0]) expectType>(result3[1]) expectType>(result3[2]) @@ -748,154 +772,164 @@ describe('useQueries', () => { expectType(result3[2].data) // initialData/placeholderData are enforced - useQueries([ - { - queryKey: key1, - queryFn: () => 'string', - placeholderData: 'string', - // @ts-expect-error (initialData: string) - initialData: 123, - }, - { - queryKey: key2, - queryFn: () => 123, - // @ts-expect-error (placeholderData: number) - placeholderData: 'string', - initialData: 123, - }, - ]) + useQueries({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + placeholderData: 'string', + // @ts-expect-error (initialData: string) + initialData: 123, + }, + { + queryKey: key2, + queryFn: () => 123, + // @ts-expect-error (placeholderData: number) + placeholderData: 'string', + initialData: 123, + }, + ], + }) // select / onSuccess / onSettled params are "indirectly" enforced - useQueries([ - // unfortunately TS will not suggest the type for you - { - queryKey: key1, - queryFn: () => 'string', - // @ts-expect-error (noImplicitAny) - onSuccess: a => null, - // @ts-expect-error (noImplicitAny) - onSettled: a => null, - }, - // however you can add a type to the callback - { - queryKey: key2, - queryFn: () => 'string', - onSuccess: (a: string) => { - expectType(a) - expectTypeNotAny(a) - }, - onSettled: (a: string | undefined) => { - expectType(a) - expectTypeNotAny(a) - }, - }, - // the type you do pass is enforced - { - queryKey: key3, - queryFn: () => 'string', - // @ts-expect-error (only accepts string) - onSuccess: (a: number) => null, - }, - { - queryKey: key4, - queryFn: () => 'string', - select: (a: string) => parseInt(a), - // @ts-expect-error (select is defined => only accepts number) - onSuccess: (a: string) => null, - onSettled: (a: number | undefined) => { - expectType(a) - expectTypeNotAny(a) - }, - }, - ]) + useQueries({ + queries: [ + // unfortunately TS will not suggest the type for you + { + queryKey: key1, + queryFn: () => 'string', + // @ts-expect-error (noImplicitAny) + onSuccess: a => null, + // @ts-expect-error (noImplicitAny) + onSettled: a => null, + }, + // however you can add a type to the callback + { + queryKey: key2, + queryFn: () => 'string', + onSuccess: (a: string) => { + expectType(a) + expectTypeNotAny(a) + }, + onSettled: (a: string | undefined) => { + expectType(a) + expectTypeNotAny(a) + }, + }, + // the type you do pass is enforced + { + queryKey: key3, + queryFn: () => 'string', + // @ts-expect-error (only accepts string) + onSuccess: (a: number) => null, + }, + { + queryKey: key4, + queryFn: () => 'string', + select: (a: string) => parseInt(a), + // @ts-expect-error (select is defined => only accepts number) + onSuccess: (a: string) => null, + onSettled: (a: number | undefined) => { + expectType(a) + expectTypeNotAny(a) + }, + }, + ], + }) // callbacks are also indirectly enforced with Array.map - useQueries( + useQueries({ // @ts-expect-error (onSuccess only accepts string) - Array(50).map((_, i) => ({ + queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), onSuccess: (_data: number) => null, - })) - ) - useQueries( - Array(50).map((_, i) => ({ + })), + }) + useQueries({ + queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), onSuccess: (_data: string) => null, - })) - ) + })), + }) // results inference works when all the handlers are defined - const result4 = useQueries([ - { - queryKey: key1, - queryFn: () => 'string', - // @ts-expect-error (noImplicitAny) - onSuccess: a => null, - // @ts-expect-error (noImplicitAny) - onSettled: a => null, - }, - { - queryKey: key2, - queryFn: () => 'string', - onSuccess: (a: string) => { - expectType(a) - expectTypeNotAny(a) - }, - onSettled: (a: string | undefined) => { - expectType(a) - expectTypeNotAny(a) - }, - }, - { - queryKey: key4, - queryFn: () => 'string', - select: (a: string) => parseInt(a), - onSuccess: (_a: number) => null, - onSettled: (a: number | undefined) => { - expectType(a) - expectTypeNotAny(a) - }, - }, - ]) + const result4 = useQueries({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + // @ts-expect-error (noImplicitAny) + onSuccess: a => null, + // @ts-expect-error (noImplicitAny) + onSettled: a => null, + }, + { + queryKey: key2, + queryFn: () => 'string', + onSuccess: (a: string) => { + expectType(a) + expectTypeNotAny(a) + }, + onSettled: (a: string | undefined) => { + expectType(a) + expectTypeNotAny(a) + }, + }, + { + queryKey: key4, + queryFn: () => 'string', + select: (a: string) => parseInt(a), + onSuccess: (_a: number) => null, + onSettled: (a: number | undefined) => { + expectType(a) + expectTypeNotAny(a) + }, + }, + ], + }) expectType>(result4[0]) expectType>(result4[1]) expectType>(result4[2]) // Array as const does not throw error - const result5 = useQueries([ - { - queryKey: ['key1'], - queryFn: () => 'string', - }, - { - queryKey: ['key1'], - queryFn: () => 123, - }, - ] as const) + const result5 = useQueries({ + queries: [ + { + queryKey: ['key1'], + queryFn: () => 'string', + }, + { + queryKey: ['key1'], + queryFn: () => 123, + }, + ], + } as const) expectType>(result5[0]) expectType>(result5[1]) // field names should be enforced - array literal - useQueries([ - { - queryKey: key1, - queryFn: () => 'string', - // @ts-expect-error (invalidField) - someInvalidField: [], - }, - ]) + useQueries({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + // @ts-expect-error (invalidField) + someInvalidField: [], + }, + ], + }) // field names should be enforced - Array.map() result - useQueries( + useQueries({ // @ts-expect-error (invalidField) - Array(10).map(() => ({ + queries: Array(10).map(() => ({ someInvalidField: '', - })) - ) + })), + }) } }) @@ -918,15 +952,17 @@ describe('useQueries', () => { }) function Queries() { - useQueries([ - { - queryKey: key1, - queryFn: async () => { - await sleep(10) - return 1 + useQueries({ + queries: [ + { + queryKey: key1, + queryFn: async () => { + await sleep(10) + return 1 + }, }, - }, - ]) + ], + }) return (
diff --git a/src/reactjs/useQueries.ts b/src/reactjs/useQueries.ts index 339f6a0937d..8f5001e123b 100644 --- a/src/reactjs/useQueries.ts +++ b/src/reactjs/useQueries.ts @@ -110,9 +110,11 @@ type QueriesResults< : // Fallback UseQueryResult[] -export function useQueries( +export function useQueries({ + queries, +}: { queries: readonly [...QueriesOptions] -): QueriesResults { +}): QueriesResults { const mountedRef = React.useRef(false) const [, forceUpdate] = React.useState(0)