1
- import { AnyAction , ThunkDispatch } from '@reduxjs/toolkit' ;
1
+ import { AnyAction , createSelector , ThunkDispatch } from '@reduxjs/toolkit' ;
2
2
import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
3
- import { useDispatch , useSelector , batch } from 'react-redux' ;
3
+ import { useDispatch , useSelector , batch , shallowEqual } from 'react-redux' ;
4
4
import {
5
5
MutationSubState ,
6
6
QueryStatus ,
7
7
QuerySubState ,
8
8
RequestStatusFlags ,
9
9
SubscriptionOptions ,
10
10
QueryKeys ,
11
+ RootState ,
11
12
} from './apiState' ;
12
- import {
13
- EndpointDefinitions ,
14
- MutationDefinition ,
15
- QueryDefinition ,
16
- QueryArgFrom ,
17
- ResultTypeFrom ,
18
- } from './endpointDefinitions' ;
19
- import { skipSelector } from './buildSelectors' ;
13
+ import { EndpointDefinitions , MutationDefinition , QueryDefinition , QueryArgFrom } from './endpointDefinitions' ;
14
+ import { QueryResultSelectorResult , skipSelector } from './buildSelectors' ;
20
15
import { QueryActionCreatorResult , MutationActionCreatorResult } from './buildActionMaps' ;
21
16
import { useShallowStableValue } from './utils' ;
22
17
import { Api , ApiEndpointMutation , ApiEndpointQuery } from './apiTypes' ;
23
- import { Id , Override } from './tsHelpers' ;
18
+ import { Id , NoInfer , Override } from './tsHelpers' ;
24
19
25
- interface QueryHookOptions extends SubscriptionOptions {
26
- skip ?: boolean ;
27
- refetchOnMountOrArgChange ?: boolean | number ;
20
+ interface QueryHooks < Definition extends QueryDefinition < any , any , any , any , any > > {
21
+ useQuery : UseQuery < Definition > ;
22
+ useQuerySubscription : UseQuerySubscription < Definition > ;
23
+ useQueryState : UseQueryState < Definition > ;
28
24
}
29
25
30
26
declare module './apiTypes' {
31
27
export interface ApiEndpointQuery <
32
28
Definition extends QueryDefinition < any , any , any , any , any > ,
33
29
Definitions extends EndpointDefinitions
34
- > {
35
- useQuery : QueryHook < Definition > ;
36
- }
30
+ > extends QueryHooks < Definition > { }
37
31
38
32
export interface ApiEndpointMutation <
39
33
Definition extends MutationDefinition < any , any , any , any , any > ,
@@ -43,12 +37,45 @@ declare module './apiTypes' {
43
37
}
44
38
}
45
39
46
- export type QueryHook < D extends QueryDefinition < any , any , any , any > > = (
40
+ export type UseQuery < D extends QueryDefinition < any , any , any , any > > = < R = UseQueryStateDefaultResult < D > > (
47
41
arg : QueryArgFrom < D > ,
48
- options ?: QueryHookOptions
49
- ) => QueryHookResult < D > ;
42
+ options ?: UseQuerySubscriptionOptions & UseQueryStateOptions < D , R >
43
+ ) => UseQueryStateResult < D , R > & ReturnType < UseQuerySubscription < D > > ;
50
44
51
- type BaseQueryHookResult < D extends QueryDefinition < any , any , any , any > > = QuerySubState < D > & {
45
+ interface UseQuerySubscriptionOptions extends SubscriptionOptions {
46
+ skip ?: boolean ;
47
+ refetchOnMountOrArgChange ?: boolean | number ;
48
+ }
49
+
50
+ export type UseQuerySubscription < D extends QueryDefinition < any , any , any , any > > = (
51
+ arg : QueryArgFrom < D > ,
52
+ options ?: UseQuerySubscriptionOptions
53
+ ) => Pick < QueryActionCreatorResult < D > , 'refetch' > ;
54
+
55
+ export type QueryStateSelector < R , D extends QueryDefinition < any , any , any , any > > = (
56
+ state : QueryResultSelectorResult < D > ,
57
+ lastResult : R | undefined ,
58
+ defaultQueryStateSelector : DefaultQueryStateSelector < D >
59
+ ) => R ;
60
+
61
+ export type DefaultQueryStateSelector < D extends QueryDefinition < any , any , any , any > > = (
62
+ state : QueryResultSelectorResult < D > ,
63
+ lastResult : Pick < UseQueryStateDefaultResult < D > , 'data' >
64
+ ) => UseQueryStateDefaultResult < D > ;
65
+
66
+ export type UseQueryState < D extends QueryDefinition < any , any , any , any > > = < R = UseQueryStateDefaultResult < D > > (
67
+ arg : QueryArgFrom < D > ,
68
+ options ?: UseQueryStateOptions < D , R >
69
+ ) => UseQueryStateResult < D , R > ;
70
+
71
+ export type UseQueryStateOptions < D extends QueryDefinition < any , any , any , any > , R > = {
72
+ skip ?: boolean ;
73
+ subSelector ?: QueryStateSelector < R , D > ;
74
+ } ;
75
+
76
+ export type UseQueryStateResult < _ extends QueryDefinition < any , any , any , any > , R > = NoInfer < R > ;
77
+
78
+ type UseQueryStateBaseResult < D extends QueryDefinition < any , any , any , any > > = QuerySubState < D > & {
52
79
/**
53
80
* Query has not started yet.
54
81
*/
@@ -69,17 +96,17 @@ type BaseQueryHookResult<D extends QueryDefinition<any, any, any, any>> = QueryS
69
96
* Query is currently in "error" state.
70
97
*/
71
98
isError : false ;
72
- } & Pick < QueryActionCreatorResult < D > , 'refetch' > ;
99
+ } ;
73
100
74
- type QueryHookResult < D extends QueryDefinition < any , any , any , any > > = Id <
75
- | Override < Extract < BaseQueryHookResult < D > , { status : QueryStatus . uninitialized } > , { isUninitialized : true } >
101
+ type UseQueryStateDefaultResult < D extends QueryDefinition < any , any , any , any > > = Id <
102
+ | Override < Extract < UseQueryStateBaseResult < D > , { status : QueryStatus . uninitialized } > , { isUninitialized : true } >
76
103
| Override <
77
- BaseQueryHookResult < D > ,
104
+ UseQueryStateBaseResult < D > ,
78
105
| { isLoading : true ; isFetching : boolean ; data : undefined }
79
106
| ( { isSuccess : true ; isFetching : boolean ; error : undefined } & Required <
80
- Pick < BaseQueryHookResult < D > , 'data' | 'fulfilledTimeStamp' >
107
+ Pick < UseQueryStateBaseResult < D > , 'data' | 'fulfilledTimeStamp' >
81
108
> )
82
- | ( { isError : true } & Required < Pick < BaseQueryHookResult < D > , 'error' > > )
109
+ | ( { isError : true } & Required < Pick < UseQueryStateBaseResult < D > , 'error' > > )
83
110
>
84
111
> ;
85
112
@@ -96,12 +123,26 @@ export type PrefetchOptions =
96
123
ifOlderThan ?: false | number ;
97
124
} ;
98
125
126
+ const defaultQueryStateSelector : DefaultQueryStateSelector < any > = ( currentState , lastResult ) => {
127
+ // data is the last known good request result we have tracked - or if none has been tracked yet the last good result for the current args
128
+ const data = ( currentState . isSuccess ? currentState . data : lastResult ?. data ) ?? currentState . data ;
129
+
130
+ // isFetching = true any time a request is in flight
131
+ const isFetching = currentState . isLoading ;
132
+ // isLoading = true only when loading while no data is present yet (initial load with no data in the cache)
133
+ const isLoading = ! data && isFetching ;
134
+ // isSuccess = true when data is present
135
+ const isSuccess = currentState . isSuccess || ( isFetching && ! ! data ) ;
136
+
137
+ return { ...currentState , data, isFetching, isLoading, isSuccess } as UseQueryStateDefaultResult < any > ;
138
+ } ;
139
+
99
140
export function buildHooks < Definitions extends EndpointDefinitions > ( {
100
141
api,
101
142
} : {
102
143
api : Api < any , Definitions , any , string > ;
103
144
} ) {
104
- return { buildQueryHook , buildMutationHook, usePrefetch } ;
145
+ return { buildQueryHooks , buildMutationHook, usePrefetch } ;
105
146
106
147
function usePrefetch < EndpointName extends QueryKeys < Definitions > > (
107
148
endpointName : EndpointName ,
@@ -117,24 +158,20 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
117
158
) ;
118
159
}
119
160
120
- function buildQueryHook ( name : string ) : QueryHook < any > {
121
- return (
161
+ function buildQueryHooks ( name : string ) : QueryHooks < any > {
162
+ const useQuerySubscription : UseQuerySubscription < any > = (
122
163
arg : any ,
123
164
{ refetchOnReconnect, refetchOnFocus, refetchOnMountOrArgChange, skip = false , pollingInterval = 0 } = { }
124
165
) => {
125
- const { select , initiate } = api . endpoints [ name ] as ApiEndpointQuery <
166
+ const { initiate } = api . endpoints [ name ] as ApiEndpointQuery <
126
167
QueryDefinition < any , any , any , any , any > ,
127
168
Definitions
128
169
> ;
129
170
const dispatch = useDispatch < ThunkDispatch < any , any , AnyAction > > ( ) ;
130
171
const stableArg = useShallowStableValue ( arg ) ;
131
172
132
- const lastData = useRef < ResultTypeFrom < Definitions [ string ] > | undefined > ( ) ;
133
173
const promiseRef = useRef < QueryActionCreatorResult < any > > ( ) ;
134
174
135
- const querySelector = useMemo ( ( ) => select ( skip ? skipSelector : stableArg ) , [ select , skip , stableArg ] ) ;
136
- const currentState = useSelector ( querySelector ) ;
137
-
138
175
useEffect ( ( ) => {
139
176
if ( skip ) {
140
177
return ;
@@ -169,33 +206,55 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
169
206
return ( ) => void promiseRef . current ?. unsubscribe ( ) ;
170
207
} , [ ] ) ;
171
208
209
+ return useMemo (
210
+ ( ) => ( {
211
+ refetch : ( ) => void promiseRef . current ?. refetch ( ) ,
212
+ } ) ,
213
+ [ ]
214
+ ) ;
215
+ } ;
216
+
217
+ const useQueryState : UseQueryState < any > = (
218
+ arg : any ,
219
+ { skip = false , subSelector = defaultQueryStateSelector as QueryStateSelector < any , any > } = { }
220
+ ) => {
221
+ const { select } = api . endpoints [ name ] as ApiEndpointQuery < QueryDefinition < any , any , any , any , any > , Definitions > ;
222
+ const stableArg = useShallowStableValue ( arg ) ;
223
+
224
+ const lastValue = useRef < any > ( ) ;
225
+
226
+ const querySelector = useMemo (
227
+ ( ) =>
228
+ createSelector (
229
+ [ select ( skip ? skipSelector : stableArg ) , ( _ : any , lastResult : any ) => lastResult ] ,
230
+ ( subState , lastResult ) => subSelector ( subState , lastResult , defaultQueryStateSelector )
231
+ ) ,
232
+ [ select , skip , stableArg , subSelector ]
233
+ ) ;
234
+
235
+ const currentState = useSelector (
236
+ ( state : RootState < Definitions , any , any > ) => querySelector ( state , lastValue . current ) ,
237
+ shallowEqual
238
+ ) ;
239
+
172
240
useEffect ( ( ) => {
173
- if ( currentState . status === QueryStatus . fulfilled ) {
174
- lastData . current = currentState . data ;
175
- }
241
+ lastValue . current = currentState ;
176
242
} , [ currentState ] ) ;
177
243
178
- const refetch = useCallback ( ( ) => void promiseRef . current ?. refetch ( ) , [ ] ) ;
179
-
180
- // data is the last known good request result
181
- const data = currentState . status === 'fulfilled' ? currentState . data : lastData . current ;
182
-
183
- const isPending = currentState . status === QueryStatus . pending ;
184
- // isLoading = true only when loading while no data is present yet (initial load)
185
- const isLoading : any = ! lastData . current && isPending ;
186
- // isFetching = true any time a request is in flight
187
- const isFetching : any = isPending ;
188
- // isSuccess = true when data is present
189
- const isSuccess : any = currentState . status === 'fulfilled' || ( isPending && ! ! data ) ;
190
-
191
- return useMemo ( ( ) => ( { ...currentState , data, isFetching, isLoading, isSuccess, refetch } ) , [
192
- currentState ,
193
- data ,
194
- isFetching ,
195
- isLoading ,
196
- isSuccess ,
197
- refetch ,
198
- ] ) ;
244
+ return currentState ;
245
+ } ;
246
+
247
+ return {
248
+ useQueryState,
249
+ useQuerySubscription,
250
+ useQuery ( arg , options ) {
251
+ const querySubscriptionResults = useQuerySubscription ( arg , options ) ;
252
+ const queryStateResults = useQueryState ( arg , options ) ;
253
+ return useMemo ( ( ) => ( { ...queryStateResults , ...querySubscriptionResults } ) , [
254
+ queryStateResults ,
255
+ querySubscriptionResults ,
256
+ ] ) ;
257
+ } ,
199
258
} ;
200
259
}
201
260
0 commit comments