Skip to content

Commit 1cbd4f4

Browse files
committed
1 parent 3c5d662 commit 1cbd4f4

File tree

6 files changed

+83
-56
lines changed

6 files changed

+83
-56
lines changed

packages/toolkit/src/query/react/buildHooks.ts

+45-25
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,7 @@ export interface MutationHooks<
7979
useMutation: UseMutation<Definition>
8080
}
8181

82-
type IdleState<Arg> = Arg extends SkipToken
83-
? { isSkipped: true }
84-
: { isSkipped: boolean }
82+
type SkippedState<Skipped extends boolean> = { isSkipped: Skipped }
8583

8684
/**
8785
* A React hook that automatically triggers fetches of data from an endpoint, 'subscribes' the component to the cached data, and reads the request status and cached data from the Redux store. The component will re-render as the loading status changes and the data becomes available.
@@ -98,16 +96,43 @@ type IdleState<Arg> = Arg extends SkipToken
9896
* - Returns the latest request status and cached data from the Redux store
9997
* - Re-renders as the request status changes and data becomes available
10098
*/
101-
export type UseQuery<D extends QueryDefinition<any, any, any, any>> = <
102-
R extends Record<string, any> = UseQueryStateDefaultResult<D>,
103-
Arg extends QueryArgFrom<D> | SkipToken = QueryArgFrom<D> | SkipToken
104-
>(
105-
arg: QueryArgFrom<D> | SkipToken,
106-
options?: UseQuerySubscriptionOptions & UseQueryStateOptions<D, R>
107-
) => UseQueryStateResult<D, R> &
108-
ReturnType<UseQuerySubscription<D>> &
109-
Suspendable &
110-
IdleState<Arg>
99+
export interface UseQuery<D extends QueryDefinition<any, any, any, any>> {
100+
// arg provided
101+
<R extends Record<string, any> = UseQueryStateDefaultResult<D>>(
102+
arg: QueryArgFrom<D>,
103+
options?: UseQuerySubscriptionOptions & UseQueryStateOptions<D, R>
104+
): UseQueryStateResult<D, R> &
105+
ReturnType<UseQuerySubscription<D>> &
106+
Suspendable &
107+
SkippedState<false>
108+
// skipped query
109+
<R extends Record<string, any> = UseQueryStateDefaultResult<D>>(
110+
arg: SkipToken,
111+
options?: UseQuerySubscriptionOptions & UseQueryStateOptions<D, R>
112+
): UseQueryStateResult<D, R> &
113+
ReturnType<UseQuerySubscription<D>> &
114+
Suspendable &
115+
SkippedState<true>
116+
<R extends Record<string, any> = UseQueryStateDefaultResult<D>>(
117+
arg: QueryArgFrom<D> | SkipToken,
118+
options?: UseQuerySubscriptionOptions & UseQueryStateOptions<D, R>
119+
): UseQueryStateResult<D, R> &
120+
ReturnType<UseQuerySubscription<D>> &
121+
Suspendable &
122+
SkippedState<boolean>
123+
}
124+
125+
/**
126+
* @internal
127+
*/
128+
type UseQueryParams<D extends QueryDefinition<any, any, any, any>> = Parameters<
129+
UseQuery<D>
130+
>
131+
132+
/**
133+
* @internal
134+
*/
135+
type AnyQueryDefinition = QueryDefinition<any, any, any, any, any>
111136

112137
interface UseQuerySubscriptionOptions extends SubscriptionOptions {
113138
/**
@@ -551,7 +576,7 @@ const createSuspendablePromise = <
551576
Definitions,
552577
Key
553578
>): Suspendable['getSuspendablePromise'] => {
554-
const retry = () => {
579+
const fetchOnce = () => {
555580
prefetch(args, {
556581
force: true,
557582
})
@@ -565,27 +590,19 @@ const createSuspendablePromise = <
565590
let pendingPromise = api.util.getRunningOperationPromise(name, args)
566591

567592
if (!pendingPromise) {
568-
prefetch(args, {
569-
force: true,
570-
})
593+
fetchOnce()
571594

572595
pendingPromise = api.util.getRunningOperationPromise(
573596
name as any,
574597
args
575598
)
576-
577-
if (!pendingPromise) {
578-
throw new Error(
579-
`[rtk-query][react]: invalid state error, expected getRunningOperationPromise(${name}, ${queryStateResults.requestId}) to be defined`
580-
)
581-
}
582599
}
583600
return pendingPromise
584601
} else if (queryStateResults.isError && !queryStateResults.isFetching) {
585602
throw new SuspenseQueryError(
586603
queryStateResults.error,
587604
queryStateResults.endpointName + '',
588-
retry
605+
fetchOnce
589606
)
590607
}
591608
}
@@ -938,7 +955,10 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
938955
[trigger, queryStateResults, info]
939956
)
940957
},
941-
useQuery(arg, options) {
958+
useQuery(
959+
arg: UseQueryParams<AnyQueryDefinition>['0'],
960+
options: UseQueryParams<AnyQueryDefinition>['1']
961+
) {
942962
const isSkipped: boolean = arg === skipToken || !!options?.skip
943963
const querySubscriptionResults = useQuerySubscription(arg, options)
944964
const queryStateResults = useQueryState(arg, {

packages/toolkit/src/query/react/exceptions.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ const computeErrorMessage = (reason: any, queryKey: string) => {
44
if (reason instanceof Error) {
55
message += reason
66
} else if (typeof reason === 'object' && reason !== null) {
7-
;[reason?.status, reason?.code, reason?.error].forEach((value) => {
8-
if (value) {
9-
message += ` ${value}`
7+
const relevantProperties = [reason?.status, reason?.code, reason?.error]
8+
9+
for (const property of relevantProperties) {
10+
if (property) {
11+
message += ` ${property}`
1012
}
11-
})
13+
}
1214
} else {
1315
message += reason
1416
}
@@ -25,5 +27,8 @@ export class SuspenseQueryError extends Error {
2527
super(computeErrorMessage(reason, endpointName))
2628
this.reason = reason
2729
this.name = 'SuspenseQueryError'
30+
31+
// https://www.typescriptlang.org/docs/handbook/2/classes.html#inheriting-built-in-types
32+
Object.setPrototypeOf(this, SuspenseQueryError.prototype)
2833
}
2934
}

packages/toolkit/src/query/react/suspense-utils.ts

+6-17
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { isPromiseLike } from '../utils/isPromiseLike'
2+
13
export interface Resource<Data> {
24
data?: Data | undefined
35
isLoading?: boolean
@@ -34,26 +36,13 @@ export type UseSuspendAllOutput<Sus extends readonly unknown[]> = {
3436
: never
3537
}
3638

37-
function isPromiseLike(val: unknown): val is PromiseLike<unknown> {
38-
return (
39-
!!val && typeof val === 'object' && typeof (val as any).then === 'function'
40-
)
41-
}
42-
43-
function getSuspendable(suspendable: Suspendable) {
39+
const getSuspendable = (suspendable: Suspendable) => {
4440
return suspendable.getSuspendablePromise()
4541
}
4642

4743
export function useSuspendAll<
48-
G extends SuspendableResource<any>,
49-
T extends SuspendableResource<any>[]
50-
>(
51-
...suspendables: readonly [G, ...T]
52-
): UseSuspendAllOutput<readonly [G, ...T]> {
53-
if (!suspendables.length) {
54-
throw new TypeError('useSuspendAll: requires one or more arguments')
55-
}
56-
44+
T extends ReadonlyArray<SuspendableResource<any>>
45+
>(...suspendables: T): UseSuspendAllOutput<T> {
5746
let promises = suspendables
5847
.map(getSuspendable)
5948
.filter(isPromiseLike) as Promise<unknown>[]
@@ -62,5 +51,5 @@ export function useSuspendAll<
6251
throw Promise.all(promises)
6352
}
6453

65-
return suspendables as UseSuspendAllOutput<readonly [G, ...T]>
54+
return suspendables as UseSuspendAllOutput<T>
6655
}

packages/toolkit/src/query/tests/buildHooks.test.tsx

+7-9
Original file line numberDiff line numberDiff line change
@@ -1542,7 +1542,7 @@ describe('hooks tests', () => {
15421542
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
15431543
tagTypes: ['User'],
15441544
endpoints: (build) => ({
1545-
checkSession: build.query<any, void>({
1545+
checkSession: build.query<any, void | undefined>({
15461546
query: () => '/me',
15471547
providesTags: ['User'],
15481548
}),
@@ -1837,7 +1837,7 @@ describe('hooks with createApi defaults set', () => {
18371837
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com/' }),
18381838
tagTypes: ['Posts'],
18391839
endpoints: (build) => ({
1840-
getPosts: build.query<PostsResponse, void>({
1840+
getPosts: build.query<PostsResponse, void | undefined>({
18411841
query: () => ({ url: 'posts' }),
18421842
providesTags: (result) =>
18431843
result ? result.map(({ id }) => ({ type: 'Posts', id })) : [],
@@ -2134,9 +2134,9 @@ describe('hooks with createApi defaults set', () => {
21342134

21352135
test('useQuery with selectFromResult option has a type error if the result is not an object', async () => {
21362136
function SelectedPost() {
2137+
// @ts-expect-error
21372138
const _res1 = api.endpoints.getPosts.useQuery(undefined, {
21382139
// selectFromResult must always return an object
2139-
// @ts-expect-error
21402140
selectFromResult: ({ data }) => data?.length ?? 0,
21412141
})
21422142

@@ -2434,18 +2434,16 @@ describe('suspense', () => {
24342434
describe('useSuspendAll', () => {
24352435
const consoleErrorSpy = jest.spyOn(console, 'error')
24362436

2437-
function ThrowsBecauseNoArgs() {
2437+
function ExceptionCausedByAnInvalidArg() {
24382438
const tuple = [
24392439
{
2440-
getSuspendablePromise() {
2440+
invalid() {
24412441
return undefined
24422442
},
24432443
},
24442444
] as const
24452445

2446-
;(tuple as unknown as any[]).splice(0, tuple.length)
2447-
2448-
useSuspendAll(...tuple)
2446+
useSuspendAll(...(tuple as any))
24492447
return <div></div>
24502448
}
24512449

@@ -2457,7 +2455,7 @@ describe('suspense', () => {
24572455
<div data-testid="error-fallback">{String(error)}</div>
24582456
)}
24592457
>
2460-
<ThrowsBecauseNoArgs />
2458+
<ExceptionCausedByAnInvalidArg />
24612459
</ErrorBoundary>
24622460
)
24632461

packages/toolkit/src/query/tests/unionTypes.test.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,10 @@ describe.skip('TS only tests', () => {
358358
getSuspendablePromise,
359359
...useQueryResultWithoutMethods
360360
} = useQueryResult
361-
expectExactType(useQueryStateResult)(useQueryResultWithoutMethods)
361+
expectExactType(useQueryStateResult)(
362+
// @ts-expect-error
363+
useQueryResultWithoutMethods
364+
)
362365
expectExactType(useQueryStateWithSelectFromResult)(
363366
// @ts-expect-error
364367
useQueryResultWithoutMethods
@@ -411,10 +414,12 @@ describe.skip('TS only tests', () => {
411414
isFetching,
412415
isError,
413416
isSuccess,
417+
isSkipped: false,
414418
isUninitialized,
415419
}
416420
},
417421
})
422+
418423
expectExactType({
419424
getSuspendablePromise: expect.any(Function),
420425
data: '' as string | number,
@@ -423,6 +428,7 @@ describe.skip('TS only tests', () => {
423428
isFetching: true,
424429
isSuccess: false,
425430
isError: false,
431+
isSkipped: false,
426432
refetch: () => {},
427433
})(result)
428434
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Thenable type guard.
3+
* @internal
4+
*/
5+
export const isPromiseLike = (val: unknown): val is PromiseLike<unknown> => {
6+
return (
7+
!!val && typeof val === 'object' && typeof (val as any).then === 'function'
8+
)
9+
}

0 commit comments

Comments
 (0)