@ic-reactor/react 3.2.0 → 3.3.1

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.
Files changed (48) hide show
  1. package/README.md +58 -4
  2. package/dist/createActorHooks.d.ts +1 -1
  3. package/dist/createActorHooks.d.ts.map +1 -1
  4. package/dist/createActorHooks.js +10 -25
  5. package/dist/createActorHooks.js.map +1 -1
  6. package/dist/createInfiniteQuery.d.ts.map +1 -1
  7. package/dist/createInfiniteQuery.js +2 -12
  8. package/dist/createInfiniteQuery.js.map +1 -1
  9. package/dist/createMutation.d.ts.map +1 -1
  10. package/dist/createMutation.js +39 -56
  11. package/dist/createMutation.js.map +1 -1
  12. package/dist/createQuery.d.ts.map +1 -1
  13. package/dist/createQuery.js +31 -39
  14. package/dist/createQuery.js.map +1 -1
  15. package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
  16. package/dist/createSuspenseInfiniteQuery.js +1 -14
  17. package/dist/createSuspenseInfiniteQuery.js.map +1 -1
  18. package/dist/createSuspenseQuery.d.ts.map +1 -1
  19. package/dist/createSuspenseQuery.js +30 -39
  20. package/dist/createSuspenseQuery.js.map +1 -1
  21. package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
  22. package/dist/hooks/useActorInfiniteQuery.js +4 -5
  23. package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
  24. package/dist/hooks/useActorMutation.d.ts +9 -2
  25. package/dist/hooks/useActorMutation.d.ts.map +1 -1
  26. package/dist/hooks/useActorMutation.js +15 -15
  27. package/dist/hooks/useActorMutation.js.map +1 -1
  28. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
  29. package/dist/hooks/useActorSuspenseInfiniteQuery.js +4 -5
  30. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
  31. package/dist/types.d.ts +26 -0
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/utils.d.ts +26 -0
  34. package/dist/utils.d.ts.map +1 -0
  35. package/dist/utils.js +41 -0
  36. package/dist/utils.js.map +1 -0
  37. package/package.json +9 -9
  38. package/src/createActorHooks.ts +32 -31
  39. package/src/createInfiniteQuery.ts +2 -19
  40. package/src/createMutation.ts +52 -69
  41. package/src/createQuery.ts +41 -52
  42. package/src/createSuspenseInfiniteQuery.ts +1 -22
  43. package/src/createSuspenseQuery.ts +42 -56
  44. package/src/hooks/useActorInfiniteQuery.ts +4 -9
  45. package/src/hooks/useActorMutation.ts +40 -14
  46. package/src/hooks/useActorSuspenseInfiniteQuery.ts +4 -9
  47. package/src/types.ts +32 -0
  48. package/src/utils.ts +52 -0
@@ -104,16 +104,11 @@ export const useActorInfiniteQuery = <
104
104
  T,
105
105
  TPageParam
106
106
  >): UseActorInfiniteQueryResult<A, M, T, TPageParam> => {
107
- // Memoize queryKey to prevent unnecessary re-calculations
107
+ // Always pass queryKey through generateQueryKey so it is merged with the
108
+ // reactor/function identity. Using the custom key verbatim would cause cache
109
+ // collisions if two different actors or methods share the same key string.
108
110
  const baseQueryKey = useMemo(
109
- () =>
110
- queryKey ??
111
- reactor.generateQueryKey(
112
- {
113
- functionName,
114
- },
115
- callConfig
116
- ),
111
+ () => reactor.generateQueryKey({ functionName, queryKey }, callConfig),
117
112
  [queryKey, reactor, functionName, callConfig]
118
113
  )
119
114
 
@@ -12,6 +12,11 @@ import {
12
12
  FunctionName,
13
13
  TransformKey,
14
14
  ReactorReturnErr,
15
+ isCanisterError,
16
+ CanisterError,
17
+ ErrResult,
18
+ ActorMethodReturnType,
19
+ TransformReturnRegistry,
15
20
  } from "@ic-reactor/core"
16
21
  import { CallConfig } from "@icp-sdk/core/agent"
17
22
 
@@ -31,6 +36,17 @@ export interface UseActorMutationParameters<
31
36
  functionName: M
32
37
  callConfig?: CallConfig
33
38
  invalidateQueries?: QueryKey[]
39
+ /**
40
+ * Callback for canister-level business logic errors.
41
+ * Called when the canister returns a Result { Err: E } variant.
42
+ * Separate from `onError`, which fires for all errors including network failures.
43
+ */
44
+ onCanisterError?: (
45
+ error: CanisterError<
46
+ TransformReturnRegistry<ErrResult<ActorMethodReturnType<A[M]>>>[T]
47
+ >,
48
+ variables: ReactorArgs<A, M, T>
49
+ ) => void
34
50
  }
35
51
 
36
52
  export type UseActorMutationConfig<
@@ -57,6 +73,7 @@ export type UseActorMutationResult<
57
73
  * reactor,
58
74
  * functionName: "transfer",
59
75
  * onSuccess: () => console.log("Success!"),
76
+ * onCanisterError: (err) => console.error("Canister Err:", err.code),
60
77
  * })
61
78
  */
62
79
  export const useActorMutation = <
@@ -68,22 +85,17 @@ export const useActorMutation = <
68
85
  functionName,
69
86
  invalidateQueries,
70
87
  onSuccess,
88
+ onError,
89
+ onCanisterError,
71
90
  callConfig,
72
91
  ...options
73
92
  }: UseActorMutationParameters<A, M, T>): UseActorMutationResult<A, M, T> => {
74
- // Memoize mutationFn to avoid creating new function on every render
75
93
  const mutationFn = useCallback(
76
- async (args: ReactorArgs<A, M, T>) => {
77
- return reactor.callMethod({
78
- functionName,
79
- callConfig,
80
- args,
81
- })
82
- },
94
+ async (args: ReactorArgs<A, M, T>) =>
95
+ reactor.callMethod({ functionName, callConfig, args }),
83
96
  [reactor, functionName, callConfig]
84
97
  )
85
98
 
86
- // Memoize onSuccess handler
87
99
  const handleSuccess = useCallback(
88
100
  async (
89
101
  ...params: Parameters<
@@ -103,21 +115,35 @@ export const useActorMutation = <
103
115
  )
104
116
  )
105
117
  }
106
- if (onSuccess) {
107
- await onSuccess(...params)
108
- }
118
+ await onSuccess?.(...params)
109
119
  },
110
120
  [reactor, invalidateQueries, onSuccess]
111
121
  )
112
122
 
113
- // Memoize mutation options
123
+ const handleError = useCallback(
124
+ (
125
+ error: ReactorReturnErr<A, M, T>,
126
+ variables: ReactorArgs<A, M, T>,
127
+ context: unknown,
128
+ mutation: unknown
129
+ ) => {
130
+ if (isCanisterError(error)) {
131
+ onCanisterError?.(error as any, variables)
132
+ }
133
+ onError?.(error, variables, context as any, mutation as any)
134
+ },
135
+ [onCanisterError, onError]
136
+ )
137
+
114
138
  const mutationOptions = useMemo(
115
139
  () => ({
116
140
  ...options,
117
141
  mutationFn,
118
142
  onSuccess: handleSuccess,
143
+ onError: handleError,
119
144
  }),
120
- [options, mutationFn, handleSuccess]
145
+ // eslint-disable-next-line react-hooks/exhaustive-deps
146
+ [mutationFn, handleSuccess, handleError]
121
147
  )
122
148
 
123
149
  return useMutation(mutationOptions, reactor.queryClient)
@@ -107,16 +107,11 @@ export const useActorSuspenseInfiniteQuery = <
107
107
  T,
108
108
  TPageParam
109
109
  >): UseActorSuspenseInfiniteQueryResult<A, M, T, TPageParam> => {
110
- // Memoize queryKey to prevent unnecessary re-calculations
110
+ // Always pass queryKey through generateQueryKey so it is merged with the
111
+ // reactor/function identity. Using the custom key verbatim would cause cache
112
+ // collisions if two different actors or methods share the same key string.
111
113
  const baseQueryKey = useMemo(
112
- () =>
113
- queryKey ??
114
- reactor.generateQueryKey(
115
- {
116
- functionName,
117
- },
118
- callConfig
119
- ),
114
+ () => reactor.generateQueryKey({ functionName, queryKey }, callConfig),
120
115
  [queryKey, reactor, functionName, callConfig]
121
116
  )
122
117
 
package/src/types.ts CHANGED
@@ -231,6 +231,18 @@ export interface BaseQueryResult<
231
231
  /** Fetch data in loader (uses ensureQueryData for cache-first) */
232
232
  fetch: () => Promise<TSelected>
233
233
 
234
+ /**
235
+ * Eagerly prefetch data into the cache without blocking.
236
+ * Useful for preloading data before navigating to a route.
237
+ *
238
+ * Unlike `fetch()`, this returns a void promise so it can be fire-and-forget.
239
+ *
240
+ * @example
241
+ * // In a route hover handler
242
+ * button.addEventListener("mouseenter", () => userQuery.prefetch())
243
+ */
244
+ prefetch: () => Promise<void>
245
+
234
246
  /** Invalidate the cache (refetches if query is active) */
235
247
  invalidate: () => Promise<void>
236
248
 
@@ -256,6 +268,26 @@ export interface BaseQueryResult<
256
268
  (): TSelected | undefined
257
269
  <TFinal>(select: (data: TSelected) => TFinal): TFinal | undefined
258
270
  }
271
+
272
+ /**
273
+ * Write raw data directly into the cache (useful for optimistic updates).
274
+ * Accepts a new value or an updater function that receives the current cached raw data.
275
+ *
276
+ * Note: The value is stored as raw (pre-select) data. Any active `select`
277
+ * transformations are automatically re-applied by React Query on the next render.
278
+ *
279
+ * @example
280
+ * // Optimistic update before a mutation
281
+ * userQuery.setData({ id: "1", name: "Alice" })
282
+ *
283
+ * // Functional update
284
+ * counterQuery.setData((prev) => (prev ?? 0) + 1)
285
+ */
286
+ setData: (
287
+ updater:
288
+ | TQueryFnData
289
+ | ((old: TQueryFnData | undefined) => TQueryFnData | undefined)
290
+ ) => TQueryFnData | undefined
259
291
  }
260
292
 
261
293
  /**
package/src/utils.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Shared internal utilities for query and mutation factories.
3
+ */
4
+
5
+ import type { QueryKey } from "@tanstack/react-query"
6
+
7
+ /**
8
+ * Internal query-key segment used to distinguish per-call factory args
9
+ * from the base query key. Not part of the public API.
10
+ */
11
+ export const FACTORY_KEY_ARGS_QUERY_KEY = "__ic_reactor_factory_key_args"
12
+
13
+ /**
14
+ * Merge a base query key, optional per-call query key, and optional key-args
15
+ * into a single query key array.
16
+ *
17
+ * Used by createInfiniteQueryFactory and createSuspenseInfiniteQueryFactory to
18
+ * ensure each unique set of factory args produces a distinct cache entry.
19
+ */
20
+ export function mergeFactoryQueryKey(
21
+ baseQueryKey?: QueryKey,
22
+ callQueryKey?: QueryKey,
23
+ keyArgs?: unknown
24
+ ): QueryKey | undefined {
25
+ const merged: unknown[] = []
26
+
27
+ if (baseQueryKey) merged.push(...baseQueryKey)
28
+ if (callQueryKey) merged.push(...callQueryKey)
29
+ if (keyArgs !== undefined)
30
+ merged.push({ [FACTORY_KEY_ARGS_QUERY_KEY]: keyArgs })
31
+
32
+ return merged.length > 0 ? merged : undefined
33
+ }
34
+
35
+ /**
36
+ * Build a chained select function that first applies the config-level select
37
+ * (if any) and then the hook-level select (if any).
38
+ *
39
+ * This enables `createQuery` / `createSuspenseQuery` to support two-level
40
+ * select chaining without duplicating the logic.
41
+ */
42
+ export function buildChainedSelect<TData, TSelected, TFinal = TSelected>(
43
+ configSelect: ((data: TData) => TSelected) | undefined,
44
+ hookSelect: ((data: TSelected) => TFinal) | undefined
45
+ ): (rawData: TData) => TSelected | TFinal {
46
+ return (rawData: TData) => {
47
+ const firstPass = configSelect
48
+ ? configSelect(rawData)
49
+ : (rawData as unknown as TSelected)
50
+ return hookSelect ? hookSelect(firstPass) : firstPass
51
+ }
52
+ }