Skip to content

feat(types): useQueries to flow through types #1527

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/src/pages/reference/useQueries.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,40 @@ The `useQueries` hook accepts an array with query option objects identical to th
**Returns**

The `useQueries` hook returns an array with all the query results.

Proposed docs to add to the `useQueries` page:

**TypeScript users**

If you're using `react-query` with TypeScript then you'll be able to benefit from type inference:

```ts
const resultWithAllTheSameTypes = useQueries(
[1, 2].map(x => ({ queryKey: `${x}`, queryFn: () => x }))
)
// resultWithAllTheSameTypes: QueryObserverResult<number, unknown>[]

const resultWithDifferentTypes = useQueries(
[1, 'two', new Date()].map(x => ({ queryKey: `${x}`, queryFn: () => x }))
)
// resultWithDifferentTypes: QueryObserverResult<string | number | Date, unknown>[]
```

In both the examples above, no types were specified and the compiler correctly inferred the types from the array passed to `useQueries`.

However, if you pass an array literal *where different elements have a `queryFn` with differing return types* then the compiler will be able to correctly infer the positional types. Consider:

```ts
const resultWithoutUsingMap = useQueries([
{ queryKey: key1, queryFn: () => 1 },
{ queryKey: key2, queryFn: () => 'two' },
])

if (result[0].data) {
const isANumber: number = result[0].data
}
if (result[1].data) {
const isAString: string = result[1].data
}
```

42 changes: 30 additions & 12 deletions src/core/queriesObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,26 @@ import type { QueryClient } from './queryClient'
import { QueryObserver } from './queryObserver'
import { Subscribable } from './subscribable'

type QueriesObserverListener = (result: QueryObserverResult[]) => void
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This had to move inline as it relies upon the generic types inside the QueriesObserver


export class QueriesObserver extends Subscribable<QueriesObserverListener> {
export class QueriesObserver<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryData = TQueryFnData
> extends Subscribable<(result: QueryObserverResult<TData, TError>[]) => void> {
private client: QueryClient
private result: QueryObserverResult[]
private queries: QueryObserverOptions[]
private observers: QueryObserver[]

constructor(client: QueryClient, queries?: QueryObserverOptions[]) {
private result: QueryObserverResult<TData, TError>[]
private queries: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData
>[]
private observers: QueryObserver<TQueryFnData, TError, TData, TQueryData>[]

constructor(
client: QueryClient,
queries?: QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>[]
) {
super()

this.client = client
Expand Down Expand Up @@ -48,12 +59,14 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
})
}

setQueries(queries: QueryObserverOptions[]): void {
setQueries(
queries: QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>[]
): void {
this.queries = queries
this.updateObservers()
}

getCurrentResult(): QueryObserverResult[] {
getCurrentResult(): QueryObserverResult<TData, TError>[] {
return this.result
}

Expand All @@ -62,7 +75,9 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {

const prevObservers = this.observers
const newObservers = this.queries.map((options, i) => {
let observer: QueryObserver | undefined = prevObservers[i]
let observer:
| QueryObserver<TQueryFnData, TError, TData, TQueryData>
| undefined = prevObservers[i]

const defaultedOptions = this.client.defaultQueryObserverOptions(options)
const hashFn = getQueryKeyHashFn(defaultedOptions)
Expand Down Expand Up @@ -110,7 +125,10 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
this.notify()
}

private onUpdate(observer: QueryObserver, result: QueryObserverResult): void {
private onUpdate(
observer: QueryObserver<TQueryFnData, TError, TData, TQueryData>,
result: QueryObserverResult<TData, TError>
): void {
const index = this.observers.indexOf(observer)
if (index !== -1) {
this.result = replaceAt(this.result, index, result)
Expand Down
88 changes: 87 additions & 1 deletion src/react/tests/useQueries.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'

import { queryKey, renderWithClient, sleep } from './utils'
import { expectType, queryKey, renderWithClient, sleep } from './utils'
import { useQueries, QueryClient, UseQueryResult, QueryCache } from '../..'

describe('useQueries', () => {
Expand Down Expand Up @@ -30,4 +30,90 @@ describe('useQueries', () => {
expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }])
expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }])
})

it('should return same data types correctly (a type test; validated by successful compilation; not runtime results)', async () => {
const key1 = queryKey()
const key2 = queryKey()
const results: number[] = []

function Page() {
const result = useQueries([
{ queryKey: key1, queryFn: () => 1 },
{ queryKey: key2, queryFn: () => 2 },
])
if (result[0].data) {
results.push(result[0].data)
}
if (result[1].data) {
results.push(result[1].data)
}
return null
}

renderWithClient(queryClient, <Page />)

await sleep(10)
})

it('should return different data types correctly (a type test; validated by successful compilation; not runtime results)', async () => {
const key1 = queryKey()
const key2 = queryKey()

function Page() {
const result = useQueries([
{ queryKey: key1, queryFn: () => 1 },
{ queryKey: key2, queryFn: () => 'two' },
])
if (result[0].data) {
expectType<number>(result[0].data)
}
if (result[1].data) {
expectType<string>(result[1].data)
}
return null
}

renderWithClient(queryClient, <Page />)

await sleep(10)
})

it('if select is provided then the return type should flow from that (a type test; validated by successful compilation; not runtime results)', async () => {
const key1 = queryKey()
const key2 = queryKey()
const key3 = queryKey()

function Page() {
const result = useQueries([
{
queryKey: key1,
queryFn: () => ({ prop: 'value' }),
select: x => x,
},
{
queryKey: key2,
queryFn: () => ({ prop: 'value' }),
// here x is unknown; we use x.prop without testing - triggering `Object is of type 'unknown'.ts(2571)`
// @ts-expect-error
select: x => x.prop as string,
},
{ queryKey: key3, queryFn: () => 1 },
])

if (result[0].data) {
expectType<unknown>(result[0].data)
}
if (result[1].data) {
expectType<string>(result[1].data)
}
if (result[2].data) {
expectType<number>(result[2].data)
}
return null
}

renderWithClient(queryClient, <Page />)

await sleep(10)
})
})
30 changes: 26 additions & 4 deletions src/react/useQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,40 @@ import { QueriesObserver } from '../core/queriesObserver'
import { useQueryClient } from './QueryClientProvider'
import { UseQueryOptions, UseQueryResult } from './types'

export function useQueries(queries: UseQueryOptions[]): UseQueryResult[] {
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T

export function useQueries<TQueries extends readonly UseQueryOptions[]>(
queries: [...TQueries]
): {
[ArrayElement in keyof TQueries]: UseQueryResult<
TQueries[ArrayElement] extends { select: infer TSelect }
? TSelect extends (data: any) => any
? ReturnType<TSelect>
: never
: Awaited<
ReturnType<
NonNullable<
Extract<TQueries[ArrayElement], UseQueryOptions>['queryFn']
>
>
>
>
} {
const queryClient = useQueryClient()

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

// Update queries
if (observer.hasListeners()) {
observer.setQueries(queries)
observer.setQueries(queries as UseQueryOptions<unknown, unknown, unknown>[])
}

const [currentResult, setCurrentResult] = React.useState(() =>
Expand All @@ -29,5 +51,5 @@ export function useQueries(queries: UseQueryOptions[]): UseQueryResult[] {
[observer]
)

return currentResult
return currentResult as any
}