@ic-reactor/react 3.12.3 → 3.12.5

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 (42) hide show
  1. package/dist/auth/authentication-manager.d.ts +1 -1
  2. package/dist/auth/authentication-manager.js +1 -1
  3. package/dist/createActorHooks.d.ts +18 -7
  4. package/dist/createActorHooks.d.ts.map +1 -1
  5. package/dist/createActorHooks.js.map +1 -1
  6. package/dist/createMutation.d.ts +1 -1
  7. package/dist/createMutation.d.ts.map +1 -1
  8. package/dist/createMutation.js +73 -17
  9. package/dist/createMutation.js.map +1 -1
  10. package/dist/defineReactor.js +1 -4
  11. package/dist/defineReactor.js.map +1 -1
  12. package/dist/hooks/createAuthHooks.d.ts +7 -2
  13. package/dist/hooks/createAuthHooks.d.ts.map +1 -1
  14. package/dist/hooks/createAuthHooks.js +22 -8
  15. package/dist/hooks/createAuthHooks.js.map +1 -1
  16. package/dist/hooks/useActorInfiniteQuery.d.ts +3 -3
  17. package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
  18. package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
  19. package/dist/hooks/useActorMethod.d.ts +21 -11
  20. package/dist/hooks/useActorMethod.d.ts.map +1 -1
  21. package/dist/hooks/useActorMethod.js +29 -14
  22. package/dist/hooks/useActorMethod.js.map +1 -1
  23. package/dist/hooks/useActorMutation.d.ts +5 -5
  24. package/dist/hooks/useActorMutation.d.ts.map +1 -1
  25. package/dist/hooks/useActorMutation.js +5 -2
  26. package/dist/hooks/useActorMutation.js.map +1 -1
  27. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +3 -3
  28. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
  29. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
  30. package/dist/types.d.ts +12 -6
  31. package/dist/types.d.ts.map +1 -1
  32. package/package.json +7 -8
  33. package/src/auth/authentication-manager.ts +1 -1
  34. package/src/createActorHooks.ts +60 -23
  35. package/src/createMutation.ts +153 -30
  36. package/src/defineReactor.ts +1 -2
  37. package/src/hooks/createAuthHooks.ts +34 -11
  38. package/src/hooks/useActorInfiniteQuery.ts +34 -6
  39. package/src/hooks/useActorMethod.ts +79 -38
  40. package/src/hooks/useActorMutation.ts +24 -9
  41. package/src/hooks/useActorSuspenseInfiniteQuery.ts +19 -6
  42. package/src/types.ts +29 -8
@@ -25,13 +25,17 @@
25
25
  * await transferMutation.execute([{ to: "aaaaa-aa", amount: "1000" }])
26
26
  */
27
27
 
28
- import { useMutation } from "@tanstack/react-query"
28
+ import {
29
+ useMutation,
30
+ type MutationFunctionContext,
31
+ } from "@tanstack/react-query"
29
32
  import type {
30
33
  Reactor,
31
34
  FunctionName,
32
35
  ReactorArgs,
33
36
  TransformKey,
34
37
  ReactorReturnOk,
38
+ ReactorReturnErr,
35
39
  } from "@ic-reactor/core"
36
40
  import { isCanisterError } from "@ic-reactor/core"
37
41
  import type {
@@ -65,9 +69,15 @@ const createMutationImpl = <
65
69
  Service,
66
70
  Method extends FunctionName<Service> = FunctionName<Service>,
67
71
  Transform extends TransformKey = "candid",
72
+ TOnMutateResult = unknown,
68
73
  >(
69
74
  reactor: Reactor<Service, Transform>,
70
- config: MutationConfig<Service, Method, Transform>
75
+ config: MutationConfig<
76
+ Service,
77
+ Method,
78
+ Transform,
79
+ TOnMutateResult | undefined
80
+ >
71
81
  ): MutationResult<Service, Method, Transform> => {
72
82
  const {
73
83
  functionName,
@@ -91,6 +101,34 @@ const createMutationImpl = <
91
101
  ): Promise<ReactorReturnOk<Service, Method, Transform>> =>
92
102
  reactor.callMethod({ functionName, args, callConfig })
93
103
 
104
+ /**
105
+ * The factory's own `onMutate` result for each call through the hook.
106
+ *
107
+ * TanStack Query stores one `onMutate` result per mutation and hands it to
108
+ * every callback. That slot holds the hook's result, which the mutation also
109
+ * exposes as `context`, so this map holds the factory's. Each key is the
110
+ * context object TanStack Query creates for a call and passes to every
111
+ * callback of that call.
112
+ */
113
+ const factoryOnMutateResults = new WeakMap<
114
+ MutationFunctionContext,
115
+ TOnMutateResult | undefined
116
+ >()
117
+
118
+ /**
119
+ * The `onMutate` result a factory callback receives. A factory without its
120
+ * own `onMutate` gets the hook's, as it always has. TanStack Query before
121
+ * 5.89 passes no context, so there a factory callback gets the hook's result
122
+ * too.
123
+ */
124
+ const factoryOnMutateResult = (
125
+ onMutateResult: unknown,
126
+ context: MutationFunctionContext | undefined
127
+ ) =>
128
+ (factoryOnMutate && context
129
+ ? factoryOnMutateResults.get(context)
130
+ : onMutateResult) as TOnMutateResult | undefined
131
+
94
132
  /**
95
133
  * Imperative execution for non-React usage.
96
134
  *
@@ -99,6 +137,9 @@ const createMutationImpl = <
99
137
  * object behaves the same through both call paths. Only hook-level callbacks
100
138
  * are absent, because there is no hook here to supply them.
101
139
  *
140
+ * The callbacks get the same `{ client, meta, mutationKey }` context
141
+ * TanStack Query passes them on the hook path.
142
+ *
102
143
  * The error is rethrown after the callbacks run, so `await execute(...)`
103
144
  * still rejects for the caller.
104
145
  *
@@ -107,19 +148,45 @@ const createMutationImpl = <
107
148
  const execute = async (
108
149
  args: ReactorArgs<Service, Method, Transform>
109
150
  ): Promise<ReactorReturnOk<Service, Method, Transform>> => {
151
+ // TanStack Query builds this for the hook path, and the callback types
152
+ // declare it as always present. None of it is mutation state, so this path
153
+ // builds the same object. Passing `undefined` made a callback that reads
154
+ // `context.client` throw here and nowhere else.
155
+ //
156
+ // The hook path takes `meta` and `mutationKey` from the client's defaulted
157
+ // options, so this runs the same spread through `defaultMutationOptions`.
158
+ // Reading `factoryOptions` alone dropped mutation defaults registered on
159
+ // the QueryClient (`defaultOptions.mutations`, `setMutationDefaults`).
160
+ const effectiveOptions = reactor.queryClient.defaultMutationOptions({
161
+ mutationKey: reactor.getQueryOptions({ functionName }).queryKey,
162
+ ...factoryOptions,
163
+ })
164
+ const context: MutationFunctionContext = {
165
+ client: reactor.queryClient,
166
+ meta: effectiveOptions.meta,
167
+ mutationKey: effectiveOptions.mutationKey,
168
+ }
169
+
110
170
  let result: ReactorReturnOk<Service, Method, Transform>
111
171
  try {
112
172
  result = await callFn(args)
113
173
  } catch (error) {
114
174
  if (isCanisterError(error)) {
115
- factoryOnCanisterError?.(error, args)
175
+ // `catch` drops the type. `callMethod` rejects with this method's
176
+ // CanisterError, the one the hook path narrows to.
177
+ factoryOnCanisterError?.(
178
+ error as Parameters<NonNullable<typeof factoryOnCanisterError>>[0],
179
+ args
180
+ )
116
181
  }
117
- // No mutation context or instance exists on the imperative path.
118
- factoryOnError?.(
182
+ // `onMutate` does not run on this path, so there is no result to pass.
183
+ // Awaited like `onSuccess` below and like the hook path, so an async
184
+ // rollback or report finishes before `execute()` rejects.
185
+ await factoryOnError?.(
119
186
  error as Parameters<NonNullable<typeof factoryOnError>>[0],
120
187
  args,
121
- undefined as never,
122
- undefined as never
188
+ undefined,
189
+ context
123
190
  )
124
191
  throw error
125
192
  }
@@ -127,18 +194,18 @@ const createMutationImpl = <
127
194
  if (factoryInvalidateQueries) {
128
195
  await invalidateAll(reactor.queryClient, factoryInvalidateQueries)
129
196
  }
130
- await factoryOnSuccess?.(
131
- result,
132
- args,
133
- undefined as never,
134
- undefined as never
135
- )
197
+ await factoryOnSuccess?.(result, args, undefined, context)
136
198
  return result
137
199
  }
138
200
 
139
201
  // Hook implementation
140
- const useMutationHook = (
141
- options?: MutationHookOptions<Service, Method, Transform>
202
+ const useMutationHook = <THookOnMutateResult = unknown>(
203
+ options?: MutationHookOptions<
204
+ Service,
205
+ Method,
206
+ Transform,
207
+ THookOnMutateResult
208
+ >
142
209
  ) => {
143
210
  const baseOptions = reactor.getQueryOptions({ functionName })
144
211
  const {
@@ -147,7 +214,12 @@ const createMutationImpl = <
147
214
  ...restOptions
148
215
  } = options ?? {}
149
216
 
150
- return useMutation(
217
+ return useMutation<
218
+ ReactorReturnOk<Service, Method, Transform>,
219
+ ReactorReturnErr<Service, Method, Transform>,
220
+ ReactorArgs<Service, Method, Transform>,
221
+ THookOnMutateResult
222
+ >(
151
223
  {
152
224
  mutationKey: baseOptions.queryKey,
153
225
  ...factoryOptions,
@@ -155,7 +227,7 @@ const createMutationImpl = <
155
227
  // Use callFn (not execute) to avoid double-invalidation:
156
228
  // factoryInvalidateQueries are handled in onSuccess below.
157
229
  mutationFn: callFn,
158
- onSuccess: async (...args) => {
230
+ onSuccess: async (data, variables, onMutateResult, context) => {
159
231
  // 1. Factory-level invalidation
160
232
  if (factoryInvalidateQueries) {
161
233
  await invalidateAll(reactor.queryClient, factoryInvalidateQueries)
@@ -165,17 +237,35 @@ const createMutationImpl = <
165
237
  await invalidateAll(reactor.queryClient, hookInvalidateQueries)
166
238
  }
167
239
  // 3. Factory onSuccess
168
- await factoryOnSuccess?.(...args)
240
+ await factoryOnSuccess?.(
241
+ data,
242
+ variables,
243
+ factoryOnMutateResult(onMutateResult, context),
244
+ context
245
+ )
169
246
  // 4. Hook onSuccess
170
- await restOptions.onSuccess?.(...args)
247
+ await restOptions.onSuccess?.(
248
+ data,
249
+ variables,
250
+ onMutateResult,
251
+ context
252
+ )
171
253
  },
172
- onError: (error, variables, context, mutation) => {
254
+ onError: async (error, variables, onMutateResult, context) => {
173
255
  if (isCanisterError(error)) {
174
256
  factoryOnCanisterError?.(error, variables)
175
257
  hookOnCanisterError?.(error, variables)
176
258
  }
177
- factoryOnError?.(error, variables, context, mutation)
178
- restOptions.onError?.(error, variables, context, mutation)
259
+ // Awaited in order, like `onSuccess`. TanStack Query holds
260
+ // `onSettled` and the settled state until this promise resolves, so
261
+ // an async `onError` must be part of it.
262
+ await factoryOnError?.(
263
+ error,
264
+ variables,
265
+ factoryOnMutateResult(onMutateResult, context),
266
+ context
267
+ )
268
+ await restOptions.onError?.(error, variables, onMutateResult, context)
179
269
  },
180
270
  // `onMutate` and `onSettled` are composed like `onSuccess`/`onError`
181
271
  // above. They used to arrive through the `...restOptions` spread, so a
@@ -183,13 +273,33 @@ const createMutationImpl = <
183
273
  // telemetry or logging simply vanished the moment any call site passed
184
274
  // its own, with no warning and no type error. Chaining is what a
185
275
  // reader who has seen `onSuccess` chain already expects.
186
- onMutate: async (...params) => {
187
- await factoryOnMutate?.(...params)
188
- return await restOptions.onMutate?.(...params)
276
+ onMutate: async (variables, context) => {
277
+ if (factoryOnMutate) {
278
+ const result = await factoryOnMutate(variables, context)
279
+ if (context) factoryOnMutateResults.set(context, result)
280
+ }
281
+ // Without a hook-level `onMutate` this is `undefined`, and
282
+ // THookOnMutateResult is then `unknown`.
283
+ return (await restOptions.onMutate?.(
284
+ variables,
285
+ context
286
+ )) as THookOnMutateResult
189
287
  },
190
- onSettled: async (...params) => {
191
- await factoryOnSettled?.(...params)
192
- await restOptions.onSettled?.(...params)
288
+ onSettled: async (data, error, variables, onMutateResult, context) => {
289
+ await factoryOnSettled?.(
290
+ data,
291
+ error,
292
+ variables,
293
+ factoryOnMutateResult(onMutateResult, context),
294
+ context
295
+ )
296
+ await restOptions.onSettled?.(
297
+ data,
298
+ error,
299
+ variables,
300
+ onMutateResult,
301
+ context
302
+ )
193
303
  },
194
304
  },
195
305
  reactor.queryClient
@@ -207,12 +317,25 @@ export function createMutation<
207
317
  Service,
208
318
  Transform extends TransformKey,
209
319
  Method extends FunctionName<Service> = FunctionName<Service>,
320
+ TOnMutateResult = unknown,
210
321
  >(
211
322
  reactor: Reactor<Service, Transform>,
212
- config: MutationConfig<NoInfer<Service>, Method, Transform>
323
+ // `execute()` runs the factory's `onSuccess` and `onError` without running
324
+ // its `onMutate`, so their `onMutate` result can also be `undefined`.
325
+ config: MutationConfig<
326
+ NoInfer<Service>,
327
+ Method,
328
+ Transform,
329
+ TOnMutateResult | undefined
330
+ >
213
331
  ): MutationResult<Service, Method, Transform> {
214
332
  return createMutationImpl(
215
333
  reactor,
216
- config as MutationConfig<Service, Method, Transform>
334
+ config as MutationConfig<
335
+ Service,
336
+ Method,
337
+ Transform,
338
+ TOnMutateResult | undefined
339
+ >
217
340
  )
218
341
  }
@@ -175,7 +175,6 @@ export function defineReactor<Service = BaseActor>(
175
175
 
176
176
  export function defineReactor<Service = BaseActor>(
177
177
  params: DefineReactorParameters | DefineDisplayReactorParameters<Service>
178
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
179
178
  ): DefineReactorResult<Service, any, any> {
180
179
  const {
181
180
  clientManager: providedClientManager,
@@ -267,7 +266,7 @@ export function defineReactor<Service = BaseActor>(
267
266
  // The Reactor / DisplayReactor union cannot be expressed through the shared
268
267
  // implementation signature, so the body is intentionally untyped here; the
269
268
  // public overloads above carry the precise types for callers.
270
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
269
+
271
270
  const reactor: any = display
272
271
  ? new DisplayReactor<Service>({
273
272
  ...reactorConfig,
@@ -11,6 +11,11 @@ export interface UseAuthReturn {
11
11
  logout: (options?: { returnTo?: string }) => Promise<void>
12
12
  isAuthenticated: boolean
13
13
  isAuthenticating: boolean
14
+ /**
15
+ * The signed-in user's principal, or `null` while signed out. A signed-out
16
+ * session still holds an anonymous `identity`, and its principal is not
17
+ * returned here.
18
+ */
14
19
  principal: Principal | null
15
20
  identity: Identity | null
16
21
  error: Error | undefined
@@ -22,6 +27,26 @@ export interface CreateAuthHooksReturn {
22
27
  useAuth: () => UseAuthReturn
23
28
  }
24
29
 
30
+ /**
31
+ * The principal both hooks return.
32
+ *
33
+ * `authenticate()` and `logout()` leave the client's anonymous identity in
34
+ * `authState.identity` with `isAuthenticated: false`, so deriving the principal
35
+ * from the identity alone reported `2vxsx-fae` for a signed-out user, and
36
+ * `principal ? <SignedIn /> : <SignedOut />` rendered the signed-in branch.
37
+ * Memoized on the identity, because `getPrincipal()` may build a new object on
38
+ * each call and the result is often a hook dependency.
39
+ */
40
+ function usePrincipal(
41
+ isAuthenticated: boolean,
42
+ identity: Identity | null
43
+ ): Principal | null {
44
+ return useMemo(
45
+ () => (isAuthenticated && identity ? identity.getPrincipal() : null),
46
+ [isAuthenticated, identity]
47
+ )
48
+ }
49
+
25
50
  /**
26
51
  * Create authentication hooks for managing user sessions with Internet Identity.
27
52
  *
@@ -32,8 +57,8 @@ export interface CreateAuthHooksReturn {
32
57
  * const { login, logout, principal, isAuthenticated } = useAuth()
33
58
  *
34
59
  * return isAuthenticated
35
- * ? <button onClick={logout}>Logout {principal?.toText()}</button>
36
- * : <button onClick={login}>Login with II</button>
60
+ * ? <button onClick={() => logout()}>Logout {principal?.toText()}</button>
61
+ * : <button onClick={() => login()}>Login with II</button>
37
62
  * }
38
63
  */
39
64
  export const createAuthHooks = (
@@ -91,10 +116,10 @@ export const createAuthHooks = (
91
116
  * const { login, logout, isAuthenticated, isAuthenticating } = useAuth()
92
117
  *
93
118
  * if (isAuthenticated) {
94
- * return <button onClick={logout}>Logout</button>
119
+ * return <button onClick={() => logout()}>Logout</button>
95
120
  * }
96
121
  * return (
97
- * <button onClick={login} disabled={isAuthenticating}>
122
+ * <button onClick={() => login()} disabled={isAuthenticating}>
98
123
  * {isAuthenticating ? "Connecting..." : "Login"}
99
124
  * </button>
100
125
  * )
@@ -125,10 +150,7 @@ export const createAuthHooks = (
125
150
  }
126
151
  }, [])
127
152
 
128
- const principal = useMemo(
129
- () => (identity ? identity.getPrincipal() : null),
130
- [identity]
131
- )
153
+ const principal = usePrincipal(isAuthenticated, identity)
132
154
 
133
155
  return {
134
156
  authenticate,
@@ -144,7 +166,8 @@ export const createAuthHooks = (
144
166
 
145
167
  /**
146
168
  * Get the current user's Principal.
147
- * Returns null if not authenticated.
169
+ * Returns null if not authenticated, including while the signed-out session
170
+ * holds the anonymous identity.
148
171
  *
149
172
  * @example
150
173
  * function UserInfo() {
@@ -154,8 +177,8 @@ export const createAuthHooks = (
154
177
  * }
155
178
  */
156
179
  const useUserPrincipal = (): Principal | null => {
157
- const { identity } = useAuthState()
158
- return identity ? identity.getPrincipal() : null
180
+ const { isAuthenticated, identity } = useAuthState()
181
+ return usePrincipal(isAuthenticated, identity)
159
182
  }
160
183
 
161
184
  return {
@@ -67,8 +67,18 @@ export type UseActorInfiniteQueryConfig<
67
67
  Method extends FunctionName<Service>,
68
68
  Transform extends TransformKey = "candid",
69
69
  TPageParam = unknown,
70
+ Selected = InfiniteData<
71
+ ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>,
72
+ TPageParam
73
+ >,
70
74
  > = Omit<
71
- UseActorInfiniteQueryParameters<Service, Method, Transform, TPageParam>,
75
+ UseActorInfiniteQueryParameters<
76
+ Service,
77
+ Method,
78
+ Transform,
79
+ TPageParam,
80
+ Selected
81
+ >,
72
82
  "reactor"
73
83
  >
74
84
 
@@ -77,11 +87,12 @@ export type UseActorInfiniteQueryResult<
77
87
  Method extends FunctionName<Service>,
78
88
  Transform extends TransformKey = "candid",
79
89
  TPageParam = unknown,
80
- > = UseInfiniteQueryResult<
81
- InfiniteData<
90
+ Selected = InfiniteData<
82
91
  ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>,
83
92
  TPageParam
84
93
  >,
94
+ > = UseInfiniteQueryResult<
95
+ Selected,
85
96
  ReactorReturnErr<Service, Method, Transform>
86
97
  >
87
98
 
@@ -102,6 +113,10 @@ export const useActorInfiniteQuery = <
102
113
  Method extends FunctionName<Service>,
103
114
  Transform extends TransformKey = "candid",
104
115
  TPageParam = unknown,
116
+ Selected = InfiniteData<
117
+ ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>,
118
+ TPageParam
119
+ >,
105
120
  >({
106
121
  reactor,
107
122
  functionName,
@@ -113,8 +128,15 @@ export const useActorInfiniteQuery = <
113
128
  Service,
114
129
  Method,
115
130
  Transform,
116
- TPageParam
117
- >): UseActorInfiniteQueryResult<Service, Method, Transform, TPageParam> => {
131
+ TPageParam,
132
+ Selected
133
+ >): UseActorInfiniteQueryResult<
134
+ Service,
135
+ Method,
136
+ Transform,
137
+ TPageParam,
138
+ Selected
139
+ > => {
118
140
  // Always pass queryKey through generateQueryKey so it is merged with the
119
141
  // reactor/function identity. Using the custom key verbatim would cause cache
120
142
  // collisions if two different actors or methods share the same key string.
@@ -173,5 +195,11 @@ export const useActorInfiniteQuery = <
173
195
  ...options,
174
196
  } as any,
175
197
  reactor.queryClient
176
- ) as UseActorInfiniteQueryResult<Service, Method, Transform, TPageParam>
198
+ ) as UseActorInfiniteQueryResult<
199
+ Service,
200
+ Method,
201
+ Transform,
202
+ TPageParam,
203
+ Selected
204
+ >
177
205
  }