@ic-reactor/react 3.12.2 → 3.12.4

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.
@@ -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
  *
@@ -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 {
@@ -14,10 +14,12 @@ import {
14
14
  TransformKey,
15
15
  ReactorArgs,
16
16
  ReactorReturnOk,
17
+ ReactorQueryData,
17
18
  ReactorReturnErr,
18
19
  FunctionType,
19
20
  } from "@ic-reactor/core"
20
21
  import { CallConfig } from "@icp-sdk/core/agent"
22
+ import { normalizeQueryData } from "../utils.js"
21
23
 
22
24
  /**
23
25
  * Configuration for useActorMethod hook.
@@ -33,10 +35,10 @@ export interface UseActorMethodParameters<
33
35
  Transform extends TransformKey = "candid",
34
36
  > extends Omit<
35
37
  QueryObserverOptions<
36
- ReactorReturnOk<Service, Method, Transform>,
38
+ ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>,
37
39
  ReactorReturnErr<Service, Method, Transform>,
38
- ReactorReturnOk<Service, Method, Transform>,
39
- ReactorReturnOk<Service, Method, Transform>,
40
+ ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>,
41
+ ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>,
40
42
  QueryKey
41
43
  >,
42
44
  "queryKey" | "queryFn"
@@ -59,8 +61,13 @@ export interface UseActorMethodParameters<
59
61
  /**
60
62
  * Callback when the method call succeeds.
61
63
  * Works for both query and mutation methods.
64
+ *
65
+ * A method's `undefined` result arrives as `null`, as in `useActorQuery`,
66
+ * for query and update methods alike.
62
67
  */
63
- onSuccess?: (data: ReactorReturnOk<Service, Method, Transform>) => void
68
+ onSuccess?: (
69
+ data: ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>
70
+ ) => void
64
71
 
65
72
  /**
66
73
  * Callback when the method call fails.
@@ -94,8 +101,13 @@ export interface UseActorMethodResult<
94
101
  Method extends FunctionName<Service> = FunctionName<Service>,
95
102
  Transform extends TransformKey = "candid",
96
103
  > {
97
- /** The returned data from the method call */
98
- data: ReactorReturnOk<Service, Method, Transform> | undefined
104
+ /**
105
+ * The returned data from the method call. A method's `undefined` result is
106
+ * `null` here, as in `useActorQuery`, so `undefined` only means no call has
107
+ * settled yet.
108
+ */
109
+ data:
110
+ ReactorQueryData<ReactorReturnOk<Service, Method, Transform>> | undefined
99
111
 
100
112
  /** Whether the method is currently executing */
101
113
  isLoading: boolean
@@ -125,11 +137,16 @@ export interface UseActorMethodResult<
125
137
  */
126
138
  call: (
127
139
  args?: ReactorArgs<Service, Method, Transform>
128
- ) => Promise<ReactorReturnOk<Service, Method, Transform> | undefined>
140
+ ) => Promise<
141
+ ReactorQueryData<ReactorReturnOk<Service, Method, Transform>> | undefined
142
+ >
129
143
 
130
144
  /**
131
- * Reset the state (clear data and error).
132
- * For queries: removes the query from cache
145
+ * Reset the state (data and error).
146
+ * For queries: resets this hook's own cache entry to its initial state, so
147
+ * `data` goes back to `initialData` when one was given and is cleared
148
+ * otherwise. An enabled hook then refetches. Other args of the same method
149
+ * are left alone.
133
150
  * For mutations: resets the mutation state
134
151
  */
135
152
  reset: () => void
@@ -138,19 +155,19 @@ export interface UseActorMethodResult<
138
155
  * For queries only: Refetch the query
139
156
  */
140
157
  refetch: () => Promise<
141
- ReactorReturnOk<Service, Method, Transform> | undefined
158
+ ReactorQueryData<ReactorReturnOk<Service, Method, Transform>> | undefined
142
159
  >
143
160
 
144
161
  // Expose underlying results for advanced use cases
145
162
  /** The raw query result (only available for query methods) */
146
163
  queryResult?: UseQueryResult<
147
- ReactorReturnOk<Service, Method, Transform>,
164
+ ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>,
148
165
  ReactorReturnErr<Service, Method, Transform>
149
166
  >
150
167
 
151
168
  /** The raw mutation result (only available for mutation methods) */
152
169
  mutationResult?: UseMutationResult<
153
- ReactorReturnOk<Service, Method, Transform>,
170
+ ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>,
154
171
  ReactorReturnErr<Service, Method, Transform>,
155
172
  ReactorArgs<Service, Method, Transform>
156
173
  >
@@ -180,6 +197,9 @@ export function useActorMethod<
180
197
  Method,
181
198
  Transform
182
199
  > {
200
+ type TData = ReactorReturnOk<Service, Method, Transform>
201
+ type TQueryData = ReactorQueryData<TData>
202
+
183
203
  // Determine if this is a query method by checking the IDL
184
204
  const isQuery = useMemo(() => {
185
205
  if (!reactor) throw new Error("Reactor instance is required")
@@ -227,7 +247,7 @@ export function useActorMethod<
227
247
  // ============================================================================
228
248
 
229
249
  const queryResult = useQuery<
230
- ReactorReturnOk<Service, Method, Transform>,
250
+ TQueryData,
231
251
  ReactorReturnErr<Service, Method, Transform>
232
252
  >(
233
253
  {
@@ -237,12 +257,19 @@ export function useActorMethod<
237
257
  // QueryClient) and `onSuccess` never fired when data came from the cache
238
258
  // or a deduped sibling. They are dispatched from the settled observer
239
259
  // result below instead.
240
- queryFn: () =>
241
- reactor.callMethod({
242
- functionName,
243
- args,
244
- callConfig,
245
- }),
260
+ //
261
+ // TanStack Query fails a query whose queryFn resolves `undefined`, with
262
+ // "data is undefined", and a successful call can resolve it. A
263
+ // DisplayReactor decodes an `opt` None to `undefined`, and a `()` query
264
+ // returns nothing. Every other query path normalizes the value to `null`.
265
+ queryFn: async () =>
266
+ normalizeQueryData<TData>(
267
+ (await reactor.callMethod({
268
+ functionName,
269
+ args,
270
+ callConfig,
271
+ })) as TData
272
+ ),
246
273
  enabled: isQuery && enabled,
247
274
  ...queryOptions,
248
275
  },
@@ -282,26 +309,30 @@ export function useActorMethod<
282
309
  // ============================================================================
283
310
 
284
311
  const mutationResult = useMutation<
285
- ReactorReturnOk<Service, Method, Transform>,
312
+ TQueryData,
286
313
  ReactorReturnErr<Service, Method, Transform>,
287
314
  ReactorArgs<Service, Method, Transform>
288
315
  >(
289
316
  {
290
317
  mutationKey: queryKey,
291
- mutationFn: async (mutationArgs) => {
292
- const result = await reactor.callMethod({
293
- functionName,
294
- args: mutationArgs ?? args,
295
- callConfig,
296
- })
297
- return result
298
- },
318
+ // Normalized like the query branch, so `data`, `call()` and `onSuccess`
319
+ // mean the same thing for both kinds of method. The hook cannot type the
320
+ // two branches apart: a Candid service type does not say which methods
321
+ // are queries.
322
+ mutationFn: async (mutationArgs) =>
323
+ normalizeQueryData<TData>(
324
+ (await reactor.callMethod({
325
+ functionName,
326
+ args: mutationArgs ?? args,
327
+ callConfig,
328
+ })) as TData
329
+ ),
299
330
  onSuccess: (data) => {
300
331
  onSuccessRef.current?.(data)
301
332
  // Invalidate specified queries after successful mutation
302
333
  if (invalidateQueries && invalidateQueries.length > 0) {
303
334
  invalidateQueries.forEach((key) => {
304
- reactor.queryClient.invalidateQueries({ queryKey: key })
335
+ void reactor.queryClient.invalidateQueries({ queryKey: key })
305
336
  })
306
337
  }
307
338
  },
@@ -319,7 +350,7 @@ export function useActorMethod<
319
350
  const call = useCallback(
320
351
  async (
321
352
  callArgs?: ReactorArgs<Service, Method, Transform>
322
- ): Promise<ReactorReturnOk<Service, Method, Transform> | undefined> => {
353
+ ): Promise<TQueryData | undefined> => {
323
354
  if (isQuery) {
324
355
  // For queries, refetch with new args if provided
325
356
  if (callArgs !== undefined) {
@@ -329,14 +360,17 @@ export function useActorMethod<
329
360
  // dedupe onto an in-flight request for the old args, returning that
330
361
  // response as though it answered this one.
331
362
  try {
332
- const result = await reactor.queryClient.fetchQuery({
363
+ const result = await reactor.queryClient.fetchQuery<TQueryData>({
333
364
  queryKey: buildQueryKey(callArgs),
334
- queryFn: () =>
335
- reactor.callMethod({
336
- functionName,
337
- args: callArgs,
338
- callConfig,
339
- }),
365
+ // Normalize for the same reason as the observer's queryFn.
366
+ queryFn: async () =>
367
+ normalizeQueryData<TData>(
368
+ (await reactor.callMethod({
369
+ functionName,
370
+ args: callArgs,
371
+ callConfig,
372
+ })) as TData
373
+ ),
340
374
  staleTime: 0,
341
375
  })
342
376
  // Dispatched here rather than by the observer effect: this result
@@ -380,7 +414,14 @@ export function useActorMethod<
380
414
 
381
415
  const reset = useCallback(() => {
382
416
  if (isQuery) {
383
- reactor.queryClient.removeQueries({ queryKey })
417
+ // Reset, not remove. `removeQueries` drops the entry without notifying
418
+ // its observers, so this hook kept rendering the old data while bound to
419
+ // a query that was no longer in the cache. No later invalidation reached
420
+ // it, including the one an identity switch runs. `resetQueries` notifies
421
+ // the observer and refetches the entry when the hook is enabled. It is
422
+ // `exact` because only this hook's entry should reset. A prefix match on
423
+ // a no-args key would also reset every args variant other hooks render.
424
+ void reactor.queryClient.resetQueries({ queryKey, exact: true })
384
425
  } else {
385
426
  mutationResult.reset()
386
427
  }
@@ -153,7 +153,7 @@ export const useActorMutation = <
153
153
  )
154
154
 
155
155
  const handleError = useCallback(
156
- (
156
+ async (
157
157
  error: ReactorReturnErr<Service, Method, Transform>,
158
158
  variables: ReactorArgs<Service, Method, Transform>,
159
159
  context: unknown,
@@ -162,7 +162,10 @@ export const useActorMutation = <
162
162
  if (isCanisterError(error)) {
163
163
  onCanisterError?.(error as any, variables)
164
164
  }
165
- onError?.(error, variables, context as any, mutation as any)
165
+ // Awaited like `onSuccess` above. TanStack Query waits for the promise
166
+ // `onError` returns before it runs `onSettled` and settles the mutation,
167
+ // and it can only wait for a promise this wrapper passes back.
168
+ await onError?.(error, variables, context as any, mutation as any)
166
169
  },
167
170
  [onCanisterError, onError]
168
171
  )