@ic-reactor/react 3.8.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -3
- package/dist/auth/authentication-manager.d.ts.map +1 -1
- package/dist/auth/authentication-manager.js +137 -21
- package/dist/auth/authentication-manager.js.map +1 -1
- package/dist/createInfiniteQuery.d.ts +11 -9
- package/dist/createInfiniteQuery.d.ts.map +1 -1
- package/dist/createInfiniteQuery.js +20 -17
- package/dist/createInfiniteQuery.js.map +1 -1
- package/dist/createMutation.d.ts.map +1 -1
- package/dist/createMutation.js +22 -2
- package/dist/createMutation.js.map +1 -1
- package/dist/createQuery.d.ts.map +1 -1
- package/dist/createQuery.js +5 -1
- package/dist/createQuery.js.map +1 -1
- package/dist/createSuspenseInfiniteQuery.d.ts +10 -9
- package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/createSuspenseInfiniteQuery.js +18 -16
- package/dist/createSuspenseInfiniteQuery.js.map +1 -1
- package/dist/createSuspenseQuery.d.ts.map +1 -1
- package/dist/createSuspenseQuery.js +5 -1
- package/dist/createSuspenseQuery.js.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.js +16 -2
- package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorMethod.d.ts.map +1 -1
- package/dist/hooks/useActorMethod.js +66 -33
- package/dist/hooks/useActorMethod.js.map +1 -1
- package/dist/hooks/useActorMutation.d.ts +8 -1
- package/dist/hooks/useActorMutation.d.ts.map +1 -1
- package/dist/hooks/useActorMutation.js +9 -1
- package/dist/hooks/useActorMutation.js.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.js +16 -2
- package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
- package/dist/types.d.ts +8 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +14 -5
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +21 -10
- package/dist/utils.js.map +1 -1
- package/package.json +2 -2
- package/src/auth/authentication-manager.ts +152 -21
- package/src/createInfiniteQuery.ts +48 -34
- package/src/createMutation.ts +32 -2
- package/src/createQuery.ts +8 -1
- package/src/createSuspenseInfiniteQuery.ts +44 -32
- package/src/createSuspenseQuery.ts +8 -1
- package/src/hooks/useActorInfiniteQuery.ts +25 -3
- package/src/hooks/useActorMethod.ts +82 -34
- package/src/hooks/useActorMutation.ts +19 -4
- package/src/hooks/useActorSuspenseInfiniteQuery.ts +25 -3
- package/src/types.ts +8 -2
- package/src/utils.ts +21 -10
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* const allPosts = data.pages.flatMap(page => page.posts)
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
import { useMemo } from "react"
|
|
27
28
|
import type {
|
|
28
29
|
Reactor,
|
|
29
30
|
FunctionName,
|
|
@@ -46,7 +47,11 @@ import {
|
|
|
46
47
|
} from "@tanstack/react-query"
|
|
47
48
|
import { CallConfig } from "@icp-sdk/core/agent"
|
|
48
49
|
import { NoInfer } from "./types.js"
|
|
49
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
buildChainedSelect,
|
|
52
|
+
mergeFactoryQueryKey,
|
|
53
|
+
normalizeQueryData,
|
|
54
|
+
} from "./utils.js"
|
|
50
55
|
|
|
51
56
|
type SuspenseInfiniteFactoryCallOptions = {
|
|
52
57
|
queryKey?: QueryKey
|
|
@@ -137,6 +142,15 @@ export interface SuspenseInfiniteQueryConfig<
|
|
|
137
142
|
queryKey?: QueryKey
|
|
138
143
|
/** Function to get args from page parameter */
|
|
139
144
|
getArgs: (pageParam: TPageParam) => ReactorArgs<Service, Method, Transform>
|
|
145
|
+
/**
|
|
146
|
+
* Narrows what the cache key derives from the call arguments.
|
|
147
|
+
*
|
|
148
|
+
* By default the key is scoped by `getArgs(initialPageParam)`, so two
|
|
149
|
+
* infinite queries on the same method with different arguments stay in
|
|
150
|
+
* separate cache entries. Supply this when those args embed the cursor and
|
|
151
|
+
* only part of them identifies the query.
|
|
152
|
+
*/
|
|
153
|
+
getKeyArgs?: (args: ReactorArgs<Service, Method, Transform>) => unknown
|
|
140
154
|
}
|
|
141
155
|
|
|
142
156
|
/**
|
|
@@ -152,17 +166,11 @@ export type SuspenseInfiniteQueryFactoryConfig<
|
|
|
152
166
|
TPageParam
|
|
153
167
|
>,
|
|
154
168
|
> = Omit<
|
|
169
|
+
// `getArgs` is supplied per factory call. `getKeyArgs` is inherited from
|
|
170
|
+
// SuspenseInfiniteQueryConfig, which now applies it for every infinite query.
|
|
155
171
|
SuspenseInfiniteQueryConfig<Service, Method, Transform, TPageParam, Selected>,
|
|
156
172
|
"getArgs"
|
|
157
|
-
>
|
|
158
|
-
/**
|
|
159
|
-
* Optional key-args derivation for factory calls.
|
|
160
|
-
* Receives the resolved args from `getArgs(initialPageParam)` and should return
|
|
161
|
-
* a stable serializable representation of the logical query identity
|
|
162
|
-
* (typically excluding pagination/cursor fields).
|
|
163
|
-
*/
|
|
164
|
-
getKeyArgs?: (args: ReactorArgs<Service, Method, Transform>) => unknown
|
|
165
|
-
}
|
|
173
|
+
>
|
|
166
174
|
|
|
167
175
|
// ============================================================================
|
|
168
176
|
// Hook Interface
|
|
@@ -301,6 +309,7 @@ const createSuspenseInfiniteQueryImpl = <
|
|
|
301
309
|
queryKey: customQueryKey,
|
|
302
310
|
initialPageParam,
|
|
303
311
|
getArgs,
|
|
312
|
+
getKeyArgs,
|
|
304
313
|
getNextPageParam,
|
|
305
314
|
getPreviousPageParam,
|
|
306
315
|
maxPages,
|
|
@@ -309,12 +318,18 @@ const createSuspenseInfiniteQueryImpl = <
|
|
|
309
318
|
...rest
|
|
310
319
|
} = config
|
|
311
320
|
|
|
312
|
-
//
|
|
321
|
+
// Fold the call arguments into the key — see the matching comment in
|
|
322
|
+
// createInfiniteQuery. Without them every infinite query on a method shares
|
|
323
|
+
// one cache entry, because the arguments live in a `getArgs` closure rather
|
|
324
|
+
// than in the config.
|
|
313
325
|
const getQueryKey = (): QueryKey => {
|
|
326
|
+
const initialArgs = getArgs(initialPageParam)
|
|
327
|
+
const keyArgs = getKeyArgs?.(initialArgs) ?? initialArgs
|
|
328
|
+
|
|
314
329
|
return reactor.generateQueryKey(
|
|
315
330
|
{
|
|
316
331
|
functionName,
|
|
317
|
-
queryKey: customQueryKey,
|
|
332
|
+
queryKey: mergeFactoryQueryKey(customQueryKey, undefined, keyArgs),
|
|
318
333
|
},
|
|
319
334
|
callConfig
|
|
320
335
|
)
|
|
@@ -370,14 +385,12 @@ const createSuspenseInfiniteQueryImpl = <
|
|
|
370
385
|
Selected,
|
|
371
386
|
TError
|
|
372
387
|
> = (options: any): any => {
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
return firstPass
|
|
380
|
-
}
|
|
388
|
+
// Memoized and identity-preserving; see buildChainedSelect for why the
|
|
389
|
+
// function identity matters to the observer's select-result cache.
|
|
390
|
+
const chainedSelect = useMemo(
|
|
391
|
+
() => buildChainedSelect(select as never, options?.select),
|
|
392
|
+
[options?.select]
|
|
393
|
+
)
|
|
381
394
|
|
|
382
395
|
return useSuspenseInfiniteQuery(
|
|
383
396
|
{
|
|
@@ -550,12 +563,13 @@ export function createSuspenseInfiniteQueryFactory<
|
|
|
550
563
|
getArgs: (pageParam: TPageParam) => ReactorArgs<Service, Method, Transform>,
|
|
551
564
|
options?: SuspenseInfiniteFactoryCallOptions
|
|
552
565
|
) => {
|
|
553
|
-
|
|
554
|
-
|
|
566
|
+
// Only the caller-supplied segments are merged here; the args-derived
|
|
567
|
+
// segment is added by the impl, which does it for every infinite query.
|
|
568
|
+
// Applying it in both places would append the args twice.
|
|
555
569
|
const queryKey = mergeFactoryQueryKey(
|
|
556
570
|
config.queryKey,
|
|
557
571
|
options?.queryKey,
|
|
558
|
-
|
|
572
|
+
undefined
|
|
559
573
|
)
|
|
560
574
|
|
|
561
575
|
return createSuspenseInfiniteQueryImpl<
|
|
@@ -565,15 +579,13 @@ export function createSuspenseInfiniteQueryFactory<
|
|
|
565
579
|
TPageParam,
|
|
566
580
|
Selected
|
|
567
581
|
>(reactor, {
|
|
568
|
-
...(
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
>
|
|
576
|
-
),
|
|
582
|
+
...(config as SuspenseInfiniteQueryFactoryConfig<
|
|
583
|
+
Service,
|
|
584
|
+
Method,
|
|
585
|
+
Transform,
|
|
586
|
+
TPageParam,
|
|
587
|
+
Selected
|
|
588
|
+
>),
|
|
577
589
|
queryKey,
|
|
578
590
|
getArgs,
|
|
579
591
|
} as SuspenseInfiniteQueryConfig<
|
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
ReactorArgs,
|
|
26
26
|
TransformKey,
|
|
27
27
|
} from "@ic-reactor/core"
|
|
28
|
+
import { useMemo } from "react"
|
|
28
29
|
import { useSuspenseQuery } from "@tanstack/react-query"
|
|
29
30
|
import type {
|
|
30
31
|
QueryFnData,
|
|
@@ -95,6 +96,12 @@ const createSuspenseQueryImpl = <
|
|
|
95
96
|
TError
|
|
96
97
|
> = (options: any): any => {
|
|
97
98
|
const baseOptions = reactor.getQueryOptions(params)
|
|
99
|
+
// Memoized so the observer's select-result cache can hit; see
|
|
100
|
+
// buildChainedSelect. `select` comes from the factory config and is stable.
|
|
101
|
+
const chainedSelect = useMemo(
|
|
102
|
+
() => buildChainedSelect(select, options?.select),
|
|
103
|
+
[options?.select]
|
|
104
|
+
)
|
|
98
105
|
return useSuspenseQuery(
|
|
99
106
|
{
|
|
100
107
|
queryKey: baseOptions.queryKey,
|
|
@@ -102,7 +109,7 @@ const createSuspenseQueryImpl = <
|
|
|
102
109
|
...rest,
|
|
103
110
|
...options,
|
|
104
111
|
queryFn: baseOptions.queryFn,
|
|
105
|
-
select:
|
|
112
|
+
select: chainedSelect,
|
|
106
113
|
},
|
|
107
114
|
reactor.queryClient
|
|
108
115
|
)
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
ReactorReturnErr,
|
|
17
17
|
} from "@ic-reactor/core"
|
|
18
18
|
import { CallConfig } from "@icp-sdk/core/agent"
|
|
19
|
-
import { normalizeQueryData } from "../utils.js"
|
|
19
|
+
import { mergeFactoryQueryKey, normalizeQueryData } from "../utils.js"
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Parameters for useActorInfiniteQuery hook.
|
|
@@ -119,8 +119,30 @@ export const useActorInfiniteQuery = <
|
|
|
119
119
|
// reactor/function identity. Using the custom key verbatim would cause cache
|
|
120
120
|
// collisions if two different actors or methods share the same key string.
|
|
121
121
|
const baseQueryKey = useMemo(
|
|
122
|
-
() =>
|
|
123
|
-
|
|
122
|
+
() =>
|
|
123
|
+
reactor.generateQueryKey(
|
|
124
|
+
{
|
|
125
|
+
functionName,
|
|
126
|
+
// Fold the call arguments into the key. They live in the `getArgs`
|
|
127
|
+
// closure rather than in the config, so without this two hooks on the
|
|
128
|
+
// same method with different arguments share one cache entry and
|
|
129
|
+
// serve each other's pages.
|
|
130
|
+
queryKey: mergeFactoryQueryKey(
|
|
131
|
+
queryKey,
|
|
132
|
+
undefined,
|
|
133
|
+
getArgs(options.initialPageParam)
|
|
134
|
+
),
|
|
135
|
+
},
|
|
136
|
+
callConfig
|
|
137
|
+
),
|
|
138
|
+
[
|
|
139
|
+
queryKey,
|
|
140
|
+
reactor,
|
|
141
|
+
functionName,
|
|
142
|
+
callConfig,
|
|
143
|
+
getArgs,
|
|
144
|
+
options.initialPageParam,
|
|
145
|
+
]
|
|
124
146
|
)
|
|
125
147
|
|
|
126
148
|
// Memoize queryFn to prevent recreation on every render
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useMemo } from "react"
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef } from "react"
|
|
2
2
|
import {
|
|
3
3
|
useQuery,
|
|
4
4
|
useMutation,
|
|
@@ -188,17 +188,30 @@ export function useActorMethod<
|
|
|
188
188
|
|
|
189
189
|
const functionType: FunctionType = isQuery ? "query" : "update"
|
|
190
190
|
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
191
|
+
// Latest callbacks, read at dispatch time: this keeps a rerendered closure
|
|
192
|
+
// from being ignored, and keeps callback identity out of the effect deps.
|
|
193
|
+
const onSuccessRef = useRef(onSuccess)
|
|
194
|
+
const onErrorRef = useRef(onError)
|
|
195
|
+
onSuccessRef.current = onSuccess
|
|
196
|
+
onErrorRef.current = onError
|
|
197
|
+
|
|
198
|
+
// Build the key for a given set of call arguments.
|
|
199
|
+
//
|
|
200
|
+
// A custom `queryKey` is appended to the canister/function prefix rather than
|
|
201
|
+
// replacing it, matching `useActorQuery`. Used verbatim it would carry no
|
|
202
|
+
// canister id, so the entry would be invisible to the canister-scoped
|
|
203
|
+
// invalidation `ClientManager.updateAgent` runs on identity change and would
|
|
204
|
+
// keep serving the previous principal's data after sign-in or sign-out.
|
|
205
|
+
const buildQueryKey = useCallback(
|
|
206
|
+
(keyArgs: ReactorArgs<Service, Method, Transform> | undefined): QueryKey =>
|
|
207
|
+
reactor.generateQueryKey(
|
|
208
|
+
{ functionName, args: keyArgs, queryKey: customQueryKey },
|
|
209
|
+
callConfig
|
|
210
|
+
),
|
|
211
|
+
[reactor, functionName, callConfig, customQueryKey]
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
const queryKey = useMemo(() => buildQueryKey(args), [buildQueryKey, args])
|
|
202
215
|
|
|
203
216
|
// ============================================================================
|
|
204
217
|
// Query Implementation
|
|
@@ -210,26 +223,51 @@ export function useActorMethod<
|
|
|
210
223
|
>(
|
|
211
224
|
{
|
|
212
225
|
queryKey,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
throw error
|
|
225
|
-
}
|
|
226
|
-
},
|
|
226
|
+
// Callbacks deliberately do NOT live here: a queryFn runs once per fetch
|
|
227
|
+
// attempt, so `onError` fired on every retry (four times with the default
|
|
228
|
+
// QueryClient) and `onSuccess` never fired when data came from the cache
|
|
229
|
+
// or a deduped sibling. They are dispatched from the settled observer
|
|
230
|
+
// result below instead.
|
|
231
|
+
queryFn: () =>
|
|
232
|
+
reactor.callMethod({
|
|
233
|
+
functionName,
|
|
234
|
+
args,
|
|
235
|
+
callConfig,
|
|
236
|
+
}),
|
|
227
237
|
enabled: isQuery && enabled,
|
|
228
238
|
...queryOptions,
|
|
229
239
|
},
|
|
230
240
|
reactor.queryClient
|
|
231
241
|
)
|
|
232
242
|
|
|
243
|
+
// Dispatch the query callbacks from the settled observer result, once per
|
|
244
|
+
// distinct outcome. TanStack v5 removed `onSuccess`/`onError` from useQuery
|
|
245
|
+
// for exactly this reason, so the settle timestamps are what identify a new
|
|
246
|
+
// result: they also advance for a cache hit on a fresh mount, which is the
|
|
247
|
+
// case that previously never notified at all.
|
|
248
|
+
const notifiedSuccessAt = useRef<number | undefined>(undefined)
|
|
249
|
+
const notifiedErrorAt = useRef<number | undefined>(undefined)
|
|
250
|
+
|
|
251
|
+
const { status, data, error, dataUpdatedAt, errorUpdatedAt } = queryResult
|
|
252
|
+
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
if (!isQuery) return
|
|
255
|
+
if (status === "success" && dataUpdatedAt !== notifiedSuccessAt.current) {
|
|
256
|
+
notifiedSuccessAt.current = dataUpdatedAt
|
|
257
|
+
onSuccessRef.current?.(data)
|
|
258
|
+
}
|
|
259
|
+
}, [isQuery, status, data, dataUpdatedAt])
|
|
260
|
+
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (!isQuery) return
|
|
263
|
+
if (status === "error" && errorUpdatedAt !== notifiedErrorAt.current) {
|
|
264
|
+
notifiedErrorAt.current = errorUpdatedAt
|
|
265
|
+
onErrorRef.current?.(
|
|
266
|
+
error as ReactorReturnErr<Service, Method, Transform>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
}, [isQuery, status, error, errorUpdatedAt])
|
|
270
|
+
|
|
233
271
|
// ============================================================================
|
|
234
272
|
// Mutation Implementation
|
|
235
273
|
// ============================================================================
|
|
@@ -250,7 +288,7 @@ export function useActorMethod<
|
|
|
250
288
|
return result
|
|
251
289
|
},
|
|
252
290
|
onSuccess: (data) => {
|
|
253
|
-
|
|
291
|
+
onSuccessRef.current?.(data)
|
|
254
292
|
// Invalidate specified queries after successful mutation
|
|
255
293
|
if (invalidateQueries && invalidateQueries.length > 0) {
|
|
256
294
|
invalidateQueries.forEach((key) => {
|
|
@@ -259,7 +297,7 @@ export function useActorMethod<
|
|
|
259
297
|
}
|
|
260
298
|
},
|
|
261
299
|
onError: (error) => {
|
|
262
|
-
|
|
300
|
+
onErrorRef.current?.(error)
|
|
263
301
|
},
|
|
264
302
|
},
|
|
265
303
|
reactor.queryClient
|
|
@@ -276,9 +314,14 @@ export function useActorMethod<
|
|
|
276
314
|
if (isQuery) {
|
|
277
315
|
// For queries, refetch with new args if provided
|
|
278
316
|
if (callArgs !== undefined) {
|
|
317
|
+
// Key on the args actually being called. Reusing the hook's
|
|
318
|
+
// mount-time key would store this result under the previous args'
|
|
319
|
+
// entry — poisoning it for every other reader — and let fetchQuery
|
|
320
|
+
// dedupe onto an in-flight request for the old args, returning that
|
|
321
|
+
// response as though it answered this one.
|
|
279
322
|
try {
|
|
280
323
|
const result = await reactor.queryClient.fetchQuery({
|
|
281
|
-
queryKey,
|
|
324
|
+
queryKey: buildQueryKey(callArgs),
|
|
282
325
|
queryFn: () =>
|
|
283
326
|
reactor.callMethod({
|
|
284
327
|
functionName,
|
|
@@ -287,10 +330,17 @@ export function useActorMethod<
|
|
|
287
330
|
}),
|
|
288
331
|
staleTime: 0,
|
|
289
332
|
})
|
|
290
|
-
|
|
333
|
+
// Dispatched here rather than by the observer effect: this result
|
|
334
|
+
// lands under the called args' key, which the mounted observer (bound
|
|
335
|
+
// to the hook's own args) does not watch. That separation is also why
|
|
336
|
+
// this can no longer double-fire the way it did when both wrote to
|
|
337
|
+
// the same key.
|
|
338
|
+
onSuccessRef.current?.(result)
|
|
291
339
|
return result
|
|
292
340
|
} catch (error) {
|
|
293
|
-
|
|
341
|
+
onErrorRef.current?.(
|
|
342
|
+
error as ReactorReturnErr<Service, Method, Transform>
|
|
343
|
+
)
|
|
294
344
|
return undefined
|
|
295
345
|
}
|
|
296
346
|
}
|
|
@@ -309,11 +359,9 @@ export function useActorMethod<
|
|
|
309
359
|
reactor,
|
|
310
360
|
functionName,
|
|
311
361
|
callConfig,
|
|
312
|
-
|
|
362
|
+
buildQueryKey,
|
|
313
363
|
queryResult,
|
|
314
364
|
mutationResult,
|
|
315
|
-
onSuccess,
|
|
316
|
-
onError,
|
|
317
365
|
]
|
|
318
366
|
)
|
|
319
367
|
|
|
@@ -35,7 +35,14 @@ export interface UseActorMutationParameters<
|
|
|
35
35
|
reactor: Reactor<Service, Transform>
|
|
36
36
|
functionName: Method
|
|
37
37
|
callConfig?: CallConfig
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Queries to invalidate upon successful mutation.
|
|
40
|
+
*
|
|
41
|
+
* `undefined` entries are skipped, so the common
|
|
42
|
+
* `[maybeQuery?.getQueryKey()]` idiom is safe when the optional query object
|
|
43
|
+
* is absent.
|
|
44
|
+
*/
|
|
45
|
+
invalidateQueries?: (QueryKey | undefined)[]
|
|
39
46
|
/**
|
|
40
47
|
* Callback for canister-level business logic errors.
|
|
41
48
|
* Called when the canister returns a Result { Err: E } variant.
|
|
@@ -126,10 +133,18 @@ export const useActorMutation = <
|
|
|
126
133
|
>
|
|
127
134
|
) => {
|
|
128
135
|
if (invalidateQueries) {
|
|
136
|
+
// Skip undefined entries. React Query reads `{ queryKey: undefined }`
|
|
137
|
+
// as "match everything", so a single undefined — which the natural
|
|
138
|
+
// `invalidateQueries: [maybeQuery?.getQueryKey()]` idiom produces
|
|
139
|
+
// whenever the optional query object is absent — would invalidate every
|
|
140
|
+
// query in the client, including an app's unrelated non-canister ones.
|
|
141
|
+
// `createMutation.invalidateAll` already filters the same way.
|
|
129
142
|
await Promise.all(
|
|
130
|
-
invalidateQueries
|
|
131
|
-
|
|
132
|
-
|
|
143
|
+
invalidateQueries
|
|
144
|
+
.filter((queryKey) => queryKey !== undefined)
|
|
145
|
+
.map((queryKey) =>
|
|
146
|
+
reactor.queryClient.invalidateQueries({ queryKey })
|
|
147
|
+
)
|
|
133
148
|
)
|
|
134
149
|
}
|
|
135
150
|
await onSuccess?.(...params)
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
ReactorReturnErr,
|
|
17
17
|
} from "@ic-reactor/core"
|
|
18
18
|
import { CallConfig } from "@icp-sdk/core/agent"
|
|
19
|
-
import { normalizeQueryData } from "../utils.js"
|
|
19
|
+
import { mergeFactoryQueryKey, normalizeQueryData } from "../utils.js"
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Parameters for useActorSuspenseInfiniteQuery hook.
|
|
@@ -129,8 +129,30 @@ export const useActorSuspenseInfiniteQuery = <
|
|
|
129
129
|
// reactor/function identity. Using the custom key verbatim would cause cache
|
|
130
130
|
// collisions if two different actors or methods share the same key string.
|
|
131
131
|
const baseQueryKey = useMemo(
|
|
132
|
-
() =>
|
|
133
|
-
|
|
132
|
+
() =>
|
|
133
|
+
reactor.generateQueryKey(
|
|
134
|
+
{
|
|
135
|
+
functionName,
|
|
136
|
+
// Fold the call arguments into the key. They live in the `getArgs`
|
|
137
|
+
// closure rather than in the config, so without this two hooks on the
|
|
138
|
+
// same method with different arguments share one cache entry and
|
|
139
|
+
// serve each other's pages.
|
|
140
|
+
queryKey: mergeFactoryQueryKey(
|
|
141
|
+
queryKey,
|
|
142
|
+
undefined,
|
|
143
|
+
getArgs(options.initialPageParam)
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
callConfig
|
|
147
|
+
),
|
|
148
|
+
[
|
|
149
|
+
queryKey,
|
|
150
|
+
reactor,
|
|
151
|
+
functionName,
|
|
152
|
+
callConfig,
|
|
153
|
+
getArgs,
|
|
154
|
+
options.initialPageParam,
|
|
155
|
+
]
|
|
134
156
|
)
|
|
135
157
|
|
|
136
158
|
// Memoize queryFn to prevent recreation on every render
|
package/src/types.ts
CHANGED
|
@@ -356,8 +356,14 @@ export interface MutationConfig<
|
|
|
356
356
|
functionName: Method
|
|
357
357
|
/** Call configuration for the actor method */
|
|
358
358
|
callConfig?: CallConfig
|
|
359
|
-
/**
|
|
360
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Queries to invalidate upon successful mutation.
|
|
361
|
+
*
|
|
362
|
+
* `undefined` entries are skipped, so the common
|
|
363
|
+
* `[maybeQuery?.getQueryKey()]` idiom is safe when the optional query object
|
|
364
|
+
* is absent.
|
|
365
|
+
*/
|
|
366
|
+
invalidateQueries?: (QueryKey | undefined)[]
|
|
361
367
|
/**
|
|
362
368
|
* Callback for canister-level business logic errors.
|
|
363
369
|
* Called when the canister returns a Result { Err: E } variant.
|
package/src/utils.ts
CHANGED
|
@@ -46,20 +46,31 @@ export function mergeFactoryQueryKey(
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* Build a chained select
|
|
50
|
-
*
|
|
49
|
+
* Build a chained select that applies the config-level select (if any) and then
|
|
50
|
+
* the hook-level select (if any).
|
|
51
51
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* Returns the caller's own function untouched when only one of the two is
|
|
53
|
+
* present, and `undefined` when neither is — allocating a wrapper only for the
|
|
54
|
+
* genuinely chained case. Identity matters here: `QueryObserver` memoizes a
|
|
55
|
+
* select result on `options.select === previousSelectFn`, so a wrapper rebuilt
|
|
56
|
+
* on every render defeats that check. The select then re-runs each render, and
|
|
57
|
+
* for a select returning a non-plain value (a `Map`, a `Date`, a `Principal`)
|
|
58
|
+
* `replaceEqualDeep` cannot structurally share the result either, so `data`
|
|
59
|
+
* gets a fresh reference every render and a `useEffect([data])` that sets state
|
|
60
|
+
* becomes an unbounded loop.
|
|
61
|
+
*
|
|
62
|
+
* Callers should still memoize the result, since the chained case allocates.
|
|
54
63
|
*/
|
|
55
64
|
export function buildChainedSelect<TData, TSelected, TFinal = TSelected>(
|
|
56
65
|
configSelect: ((data: TData) => TSelected) | undefined,
|
|
57
66
|
hookSelect: ((data: TSelected) => TFinal) | undefined
|
|
58
|
-
): (rawData: TData) => TSelected | TFinal {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
): ((rawData: TData) => TSelected | TFinal) | undefined {
|
|
68
|
+
if (!configSelect) {
|
|
69
|
+
// `hookSelect` receives the raw data when there is no config-level select,
|
|
70
|
+
// matching the previous behaviour.
|
|
71
|
+
return hookSelect as unknown as
|
|
72
|
+
((rawData: TData) => TSelected | TFinal) | undefined
|
|
64
73
|
}
|
|
74
|
+
if (!hookSelect) return configSelect
|
|
75
|
+
return (rawData: TData) => hookSelect(configSelect(rawData))
|
|
65
76
|
}
|