Skip to content

Commit 4441eef

Browse files
msutkowskiphryneas
andauthored
Add unwrap to QueryActionCreatorResult and update LazyQueryTrigger (#1701)
* Add unwrap to QueryActionCreatorResult and update LazyQueryTrigger * Add test for the return value of lazy query's trigger * Fix types for lazy query :fingers_crossed: * minor refactor, add test for positive case * Use QueryResultSelectorResult instead of QuerySubState * Update buildHooks test and type expectations for useLazyQuery unwrapping * Update UseLazyQueryTrigger signature * Remove extra unwrap and add queryString types * remove import, fix test Co-authored-by: Lenz Weber <[email protected]>
1 parent 907a948 commit 4441eef

File tree

6 files changed

+171
-32
lines changed

6 files changed

+171
-32
lines changed

docs/rtk-query/api/created-api/hooks.mdx

+12-1
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,18 @@ type UseLazyQueryOptions = {
490490
selectFromResult?: (result: UseQueryStateDefaultResult) => any
491491
}
492492

493-
type UseLazyQueryTrigger = (arg: any) => void
493+
type UseLazyQueryTrigger<T> = (arg: any) => Promise<
494+
QueryResultSelectorResult
495+
> & {
496+
arg: unknown // Whatever argument was provided to the query
497+
requestId: string // A string generated by RTK Query
498+
subscriptionOptions: SubscriptionOptions // The values used for the query subscription
499+
abort: () => void // A method to cancel the query promise
500+
unwrap: () => Promise<T> // A method to unwrap the query call and provide the raw response/error
501+
unsubscribe: () => void // A method used to manually unsubscribe from the query results
502+
refetch: () => void // A method used to re-run the query. In most cases when using a lazy query, you will never use this and should prefer to call the trigger again.
503+
updateSubscriptionOptions: (options: SubscriptionOptions) () => void // A method used to update the subscription options (eg. pollingInterval)
504+
}
494505

495506
type UseQueryStateResult<T> = {
496507
// Base query state

packages/toolkit/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"@types/json-stringify-safe": "^5.0.0",
3838
"@types/nanoid": "^2.1.0",
3939
"@types/node": "^10.14.4",
40+
"@types/query-string": "^6.3.0",
4041
"@types/react": "^17.0.3",
4142
"@types/react-dom": "^17.0.3",
4243
"@types/react-redux": "^7.1.16",

packages/toolkit/src/query/core/buildInitiate.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import type {
88
import { DefinitionType } from '../endpointDefinitions'
99
import type { QueryThunk, MutationThunk } from './buildThunks'
1010
import type { AnyAction, ThunkAction, SerializedError } from '@reduxjs/toolkit'
11-
import type { QuerySubState, SubscriptionOptions, RootState } from './apiState'
11+
import type { SubscriptionOptions, RootState } from './apiState'
1212
import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
1313
import type { Api, ApiContext } from '../apiTypes'
1414
import type { ApiEndpointQuery } from './module'
1515
import type { BaseQueryError } from '../baseQueryTypes'
16+
import type { QueryResultSelectorResult } from './buildSelectors'
1617

1718
declare module './module' {
1819
export interface ApiEndpointQuery<
@@ -47,11 +48,12 @@ type StartQueryActionCreator<
4748

4849
export type QueryActionCreatorResult<
4950
D extends QueryDefinition<any, any, any, any>
50-
> = Promise<QuerySubState<D>> & {
51+
> = Promise<QueryResultSelectorResult<D>> & {
5152
arg: QueryArgFrom<D>
5253
requestId: string
5354
subscriptionOptions: SubscriptionOptions | undefined
5455
abort(): void
56+
unwrap(): Promise<ResultTypeFrom<D>>
5557
unsubscribe(): void
5658
refetch(): void
5759
updateSubscriptionOptions(options: SubscriptionOptions): void
@@ -273,7 +275,7 @@ Features like automatic cache collection, automatic refetching etc. will not be
273275
})
274276
const thunkResult = dispatch(thunk)
275277
middlewareWarning(getState)
276-
const { requestId, abort } = thunkResult
278+
const { requestId, abort, unwrap } = thunkResult
277279
const statePromise: QueryActionCreatorResult<any> = Object.assign(
278280
Promise.all([runningQueries[queryCacheKey], thunkResult]).then(() =>
279281
(api.endpoints[endpointName] as ApiEndpointQuery<any, any>).select(
@@ -285,6 +287,7 @@ Features like automatic cache collection, automatic refetching etc. will not be
285287
requestId,
286288
subscriptionOptions,
287289
abort,
290+
unwrap,
288291
refetch() {
289292
dispatch(
290293
queryAction(arg, { subscribe: false, forceRefetch: true })
@@ -339,7 +342,7 @@ Features like automatic cache collection, automatic refetching etc. will not be
339342
})
340343
const thunkResult = dispatch(thunk)
341344
middlewareWarning(getState)
342-
const { requestId, abort } = thunkResult
345+
const { requestId, abort, unwrap } = thunkResult
343346
const returnValuePromise = thunkResult
344347
.unwrap()
345348
.then((data) => ({ data }))
@@ -353,7 +356,7 @@ Features like automatic cache collection, automatic refetching etc. will not be
353356
arg: thunkResult.arg,
354357
requestId,
355358
abort,
356-
unwrap: thunkResult.unwrap,
359+
unwrap,
357360
unsubscribe: reset,
358361
reset,
359362
})

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

+25-6
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,25 @@ export type LazyQueryTrigger<D extends QueryDefinition<any, any, any, any>> = {
202202
*
203203
* By default, this will start a new request even if there is already a value in the cache.
204204
* If you want to use the cache value and only start a request if there is no cache value, set the second argument to `true`.
205+
*
206+
* @remarks
207+
* If you need to access the error or success payload immediately after a lazy query, you can chain .unwrap().
208+
*
209+
* @example
210+
* ```ts
211+
* // codeblock-meta title="Using .unwrap with async await"
212+
* try {
213+
* const payload = await getUserById(1).unwrap();
214+
* console.log('fulfilled', payload)
215+
* } catch (error) {
216+
* console.error('rejected', error);
217+
* }
218+
* ```
205219
*/
206-
(arg: QueryArgFrom<D>, preferCacheValue?: boolean): void
220+
(
221+
arg: QueryArgFrom<D>,
222+
preferCacheValue?: boolean
223+
): QueryActionCreatorResult<D>
207224
}
208225

209226
/**
@@ -221,10 +238,7 @@ export type UseLazyQuerySubscription<
221238
D extends QueryDefinition<any, any, any, any>
222239
> = (
223240
options?: SubscriptionOptions
224-
) => readonly [
225-
(arg: QueryArgFrom<D>) => void,
226-
QueryArgFrom<D> | UninitializedValue
227-
]
241+
) => readonly [LazyQueryTrigger<D>, QueryArgFrom<D> | UninitializedValue]
228242

229243
export type QueryStateSelector<
230244
R extends Record<string, any>,
@@ -681,17 +695,22 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
681695

682696
const trigger = useCallback(
683697
function (arg: any, preferCacheValue = false) {
698+
let promise: QueryActionCreatorResult<any>
699+
684700
batch(() => {
685701
promiseRef.current?.unsubscribe()
686702

687-
promiseRef.current = dispatch(
703+
promiseRef.current = promise = dispatch(
688704
initiate(arg, {
689705
subscriptionOptions: subscriptionOptionsRef.current,
690706
forceRefetch: !preferCacheValue,
691707
})
692708
)
709+
693710
setArg(arg)
694711
})
712+
713+
return promise!
695714
},
696715
[dispatch, initiate]
697716
)

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

+104-9
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import userEvent from '@testing-library/user-event'
1010
import { rest } from 'msw'
1111
import {
1212
actionsReducer,
13+
ANY,
1314
expectExactType,
1415
expectType,
1516
setupApiStore,
@@ -48,8 +49,10 @@ const api = createApi({
4849
}
4950
},
5051
endpoints: (build) => ({
51-
getUser: build.query<any, number>({
52-
query: (arg) => arg,
52+
getUser: build.query<{ name: string }, number>({
53+
query: () => ({
54+
body: { name: 'Timmy' },
55+
}),
5356
}),
5457
getIncrementedAmount: build.query<any, void>({
5558
query: () => ({
@@ -774,6 +777,98 @@ describe('hooks tests', () => {
774777
.actions.filter(api.internalActions.unsubscribeQueryResult.match)
775778
).toHaveLength(4)
776779
})
780+
781+
test('useLazyQuery hook callback returns various properties to handle the result', async () => {
782+
function User() {
783+
const [getUser] = api.endpoints.getUser.useLazyQuery()
784+
const [{ successMsg, errMsg, isAborted }, setValues] = React.useState({
785+
successMsg: '',
786+
errMsg: '',
787+
isAborted: false,
788+
})
789+
790+
const handleClick = (abort: boolean) => async () => {
791+
const res = getUser(1)
792+
793+
// no-op simply for clearer type assertions
794+
res.then((result) => {
795+
if (result.isSuccess) {
796+
expectType<{
797+
data: {
798+
name: string
799+
}
800+
}>(result)
801+
}
802+
if (result.isError) {
803+
expectType<{
804+
error: { status: number; data: unknown } | SerializedError
805+
}>(result)
806+
}
807+
})
808+
809+
expectType<number>(res.arg)
810+
expectType<string>(res.requestId)
811+
expectType<() => void>(res.abort)
812+
expectType<() => Promise<{ name: string }>>(res.unwrap)
813+
expectType<() => void>(res.unsubscribe)
814+
expectType<(options: SubscriptionOptions) => void>(
815+
res.updateSubscriptionOptions
816+
)
817+
expectType<() => void>(res.refetch)
818+
819+
// abort the query immediately to force an error
820+
if (abort) res.abort()
821+
res
822+
.unwrap()
823+
.then((result) => {
824+
expectType<{ name: string }>(result)
825+
setValues({
826+
successMsg: `Successfully fetched user ${result.name}`,
827+
errMsg: '',
828+
isAborted: false,
829+
})
830+
})
831+
.catch((err) => {
832+
setValues({
833+
successMsg: '',
834+
errMsg: `An error has occurred fetching userId: ${res.arg}`,
835+
isAborted: err.name === 'AbortError',
836+
})
837+
})
838+
}
839+
840+
return (
841+
<div>
842+
<button onClick={handleClick(false)}>
843+
Fetch User successfully
844+
</button>
845+
<button onClick={handleClick(true)}>Fetch User and abort</button>
846+
<div>{successMsg}</div>
847+
<div>{errMsg}</div>
848+
<div>{isAborted ? 'Request was aborted' : ''}</div>
849+
</div>
850+
)
851+
}
852+
853+
render(<User />, { wrapper: storeRef.wrapper })
854+
expect(screen.queryByText(/An error has occurred/i)).toBeNull()
855+
expect(screen.queryByText(/Successfully fetched user/i)).toBeNull()
856+
expect(screen.queryByText('Request was aborted')).toBeNull()
857+
858+
fireEvent.click(
859+
screen.getByRole('button', { name: 'Fetch User and abort' })
860+
)
861+
await screen.findByText('An error has occurred fetching userId: 1')
862+
expect(screen.queryByText(/Successfully fetched user/i)).toBeNull()
863+
screen.getByText('Request was aborted')
864+
865+
fireEvent.click(
866+
screen.getByRole('button', { name: 'Fetch User successfully' })
867+
)
868+
await screen.findByText('Successfully fetched user Timmy')
869+
expect(screen.queryByText(/An error has occurred/i)).toBeNull()
870+
expect(screen.queryByText('Request was aborted')).toBeNull()
871+
})
777872
})
778873

779874
describe('useMutation', () => {
@@ -844,7 +939,7 @@ describe('hooks tests', () => {
844939

845940
// no-op simply for clearer type assertions
846941
res.then((result) => {
847-
expectExactType<
942+
expectType<
848943
| {
849944
error: { status: number; data: unknown } | SerializedError
850945
}
@@ -998,7 +1093,7 @@ describe('hooks tests', () => {
9981093
expect(
9991094
api.endpoints.getUser.select(USER_ID)(storeRef.store.getState() as any)
10001095
).toEqual({
1001-
data: {},
1096+
data: { name: 'Timmy' },
10021097
endpointName: 'getUser',
10031098
error: undefined,
10041099
fulfilledTimeStamp: expect.any(Number),
@@ -1019,7 +1114,7 @@ describe('hooks tests', () => {
10191114
expect(
10201115
api.endpoints.getUser.select(USER_ID)(storeRef.store.getState() as any)
10211116
).toEqual({
1022-
data: {},
1117+
data: { name: 'Timmy' },
10231118
endpointName: 'getUser',
10241119
fulfilledTimeStamp: expect.any(Number),
10251120
isError: false,
@@ -1067,7 +1162,7 @@ describe('hooks tests', () => {
10671162
expect(
10681163
api.endpoints.getUser.select(USER_ID)(storeRef.store.getState() as any)
10691164
).toEqual({
1070-
data: {},
1165+
data: { name: 'Timmy' },
10711166
endpointName: 'getUser',
10721167
fulfilledTimeStamp: expect.any(Number),
10731168
isError: false,
@@ -1085,7 +1180,7 @@ describe('hooks tests', () => {
10851180
expect(
10861181
api.endpoints.getUser.select(USER_ID)(storeRef.store.getState() as any)
10871182
).toEqual({
1088-
data: {},
1183+
data: { name: 'Timmy' },
10891184
endpointName: 'getUser',
10901185
fulfilledTimeStamp: expect.any(Number),
10911186
isError: false,
@@ -1135,7 +1230,7 @@ describe('hooks tests', () => {
11351230
expect(
11361231
api.endpoints.getUser.select(USER_ID)(storeRef.store.getState() as any)
11371232
).toEqual({
1138-
data: {},
1233+
data: { name: 'Timmy' },
11391234
endpointName: 'getUser',
11401235
fulfilledTimeStamp: expect.any(Number),
11411236
isError: false,
@@ -1155,7 +1250,7 @@ describe('hooks tests', () => {
11551250
expect(
11561251
api.endpoints.getUser.select(USER_ID)(storeRef.store.getState() as any)
11571252
).toEqual({
1158-
data: {},
1253+
data: { name: 'Timmy' },
11591254
endpointName: 'getUser',
11601255
fulfilledTimeStamp: expect.any(Number),
11611256
isError: false,

yarn.lock

+21-11
Original file line numberDiff line numberDiff line change
@@ -5058,6 +5058,7 @@ __metadata:
50585058
"@types/json-stringify-safe": ^5.0.0
50595059
"@types/nanoid": ^2.1.0
50605060
"@types/node": ^10.14.4
5061+
"@types/query-string": ^6.3.0
50615062
"@types/react": ^17.0.3
50625063
"@types/react-dom": ^17.0.3
50635064
"@types/react-redux": ^7.1.16
@@ -6133,6 +6134,15 @@ __metadata:
61336134
languageName: node
61346135
linkType: hard
61356136

6137+
"@types/query-string@npm:^6.3.0":
6138+
version: 6.3.0
6139+
resolution: "@types/query-string@npm:6.3.0"
6140+
dependencies:
6141+
query-string: "*"
6142+
checksum: 7d507aea24e650548bc8a164ae695deb1eaf7a566fdaeb53ec3f89605dbd2c5201daf9b76c31843bc8accc01e866df8b2a06557514ad8014c3af99c664cd0b5c
6143+
languageName: node
6144+
linkType: hard
6145+
61366146
"@types/react-dom@npm:17.0.0":
61376147
version: 17.0.0
61386148
resolution: "@types/react-dom@npm:17.0.0"
@@ -20272,17 +20282,7 @@ fsevents@^1.2.7:
2027220282
languageName: node
2027320283
linkType: hard
2027420284

20275-
"query-string@npm:^4.1.0":
20276-
version: 4.3.4
20277-
resolution: "query-string@npm:4.3.4"
20278-
dependencies:
20279-
object-assign: ^4.1.0
20280-
strict-uri-encode: ^1.0.0
20281-
checksum: 3b2bae6a8454cf0edf11cf1aa4d1f920398bbdabc1c39222b9bb92147e746fcd97faf00e56f494728fb66b2961b495ba0fde699d5d3bd06b11472d664b36c6cf
20282-
languageName: node
20283-
linkType: hard
20284-
20285-
"query-string@npm:^7.0.1":
20285+
"query-string@npm:*, query-string@npm:^7.0.1":
2028620286
version: 7.0.1
2028720287
resolution: "query-string@npm:7.0.1"
2028820288
dependencies:
@@ -20294,6 +20294,16 @@ fsevents@^1.2.7:
2029420294
languageName: node
2029520295
linkType: hard
2029620296

20297+
"query-string@npm:^4.1.0":
20298+
version: 4.3.4
20299+
resolution: "query-string@npm:4.3.4"
20300+
dependencies:
20301+
object-assign: ^4.1.0
20302+
strict-uri-encode: ^1.0.0
20303+
checksum: 3b2bae6a8454cf0edf11cf1aa4d1f920398bbdabc1c39222b9bb92147e746fcd97faf00e56f494728fb66b2961b495ba0fde699d5d3bd06b11472d664b36c6cf
20304+
languageName: node
20305+
linkType: hard
20306+
2029720307
"querystring-es3@npm:^0.2.0":
2029820308
version: 0.2.1
2029920309
resolution: "querystring-es3@npm:0.2.1"

0 commit comments

Comments
 (0)