@ic-reactor/react 3.0.0-beta.8 → 3.0.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.
Files changed (46) hide show
  1. package/README.md +11 -10
  2. package/dist/createActorHooks.d.ts +2 -0
  3. package/dist/createActorHooks.d.ts.map +1 -1
  4. package/dist/createActorHooks.js +2 -0
  5. package/dist/createActorHooks.js.map +1 -1
  6. package/dist/createMutation.d.ts.map +1 -1
  7. package/dist/createMutation.js +4 -0
  8. package/dist/createMutation.js.map +1 -1
  9. package/dist/hooks/index.d.ts +18 -5
  10. package/dist/hooks/index.d.ts.map +1 -1
  11. package/dist/hooks/index.js +15 -5
  12. package/dist/hooks/index.js.map +1 -1
  13. package/dist/hooks/useActorInfiniteQuery.d.ts +13 -11
  14. package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
  15. package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
  16. package/dist/hooks/useActorMethod.d.ts +105 -0
  17. package/dist/hooks/useActorMethod.d.ts.map +1 -0
  18. package/dist/hooks/useActorMethod.js +192 -0
  19. package/dist/hooks/useActorMethod.js.map +1 -0
  20. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +13 -10
  21. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
  22. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
  23. package/package.json +7 -6
  24. package/src/createActorHooks.ts +146 -0
  25. package/src/createAuthHooks.ts +137 -0
  26. package/src/createInfiniteQuery.ts +471 -0
  27. package/src/createMutation.ts +163 -0
  28. package/src/createQuery.ts +197 -0
  29. package/src/createSuspenseInfiniteQuery.ts +478 -0
  30. package/src/createSuspenseQuery.ts +215 -0
  31. package/src/hooks/index.ts +93 -0
  32. package/src/hooks/useActorInfiniteQuery.test.tsx +457 -0
  33. package/src/hooks/useActorInfiniteQuery.ts +134 -0
  34. package/src/hooks/useActorMethod.test.tsx +798 -0
  35. package/src/hooks/useActorMethod.ts +397 -0
  36. package/src/hooks/useActorMutation.test.tsx +220 -0
  37. package/src/hooks/useActorMutation.ts +124 -0
  38. package/src/hooks/useActorQuery.test.tsx +287 -0
  39. package/src/hooks/useActorQuery.ts +110 -0
  40. package/src/hooks/useActorSuspenseInfiniteQuery.test.tsx +472 -0
  41. package/src/hooks/useActorSuspenseInfiniteQuery.ts +137 -0
  42. package/src/hooks/useActorSuspenseQuery.test.tsx +254 -0
  43. package/src/hooks/useActorSuspenseQuery.ts +112 -0
  44. package/src/index.ts +21 -0
  45. package/src/types.ts +435 -0
  46. package/src/validation.ts +202 -0
@@ -0,0 +1,397 @@
1
+ import { useCallback, useMemo } from "react"
2
+ import {
3
+ useQuery,
4
+ useMutation,
5
+ type UseQueryResult,
6
+ type UseMutationResult,
7
+ type QueryKey,
8
+ type QueryObserverOptions,
9
+ } from "@tanstack/react-query"
10
+ import {
11
+ Reactor,
12
+ BaseActor,
13
+ FunctionName,
14
+ TransformKey,
15
+ ReactorArgs,
16
+ ReactorReturnOk,
17
+ ReactorReturnErr,
18
+ FunctionType,
19
+ } from "@ic-reactor/core"
20
+ import { CallConfig } from "@icp-sdk/core/agent"
21
+
22
+ /**
23
+ * Configuration for useActorMethod hook.
24
+ * Extends react-query's QueryObserverOptions with custom reactor params.
25
+ *
26
+ * This is a unified hook that handles both query and mutation methods.
27
+ * Query-specific options (like refetchInterval) only apply to query methods.
28
+ * Mutation-specific options (like invalidateQueries) only apply to mutation methods.
29
+ */
30
+ export interface UseActorMethodParameters<
31
+ A = BaseActor,
32
+ M extends FunctionName<A> = FunctionName<A>,
33
+ T extends TransformKey = "candid",
34
+ > extends Omit<
35
+ QueryObserverOptions<
36
+ ReactorReturnOk<A, M, T>,
37
+ ReactorReturnErr<A, M, T>,
38
+ ReactorReturnOk<A, M, T>,
39
+ ReactorReturnOk<A, M, T>,
40
+ QueryKey
41
+ >,
42
+ "queryKey" | "queryFn"
43
+ > {
44
+ /** The reactor instance to use for method calls */
45
+ reactor: Reactor<A, T>
46
+
47
+ /** The method name to call on the canister */
48
+ functionName: M
49
+
50
+ /** Arguments to pass to the method (optional for parameterless methods) */
51
+ args?: ReactorArgs<A, M, T>
52
+
53
+ /** Agent call configuration (effectiveCanisterId, etc.) */
54
+ callConfig?: CallConfig
55
+
56
+ /** Custom query key (auto-generated if not provided) */
57
+ queryKey?: QueryKey
58
+
59
+ /**
60
+ * Callback when the method call succeeds.
61
+ * Works for both query and mutation methods.
62
+ */
63
+ onSuccess?: (data: ReactorReturnOk<A, M, T>) => void
64
+
65
+ /**
66
+ * Callback when the method call fails.
67
+ * Works for both query and mutation methods.
68
+ */
69
+ onError?: (error: ReactorReturnErr<A, M, T>) => void
70
+
71
+ /**
72
+ * Query keys to invalidate after a successful mutation.
73
+ * Only applies to mutation methods (updates).
74
+ */
75
+ invalidateQueries?: QueryKey[]
76
+ }
77
+
78
+ /**
79
+ * Configuration type for bound useActorMethod hook (reactor omitted).
80
+ * For use with createActorHooks.
81
+ */
82
+ export type UseActorMethodConfig<
83
+ A = BaseActor,
84
+ M extends FunctionName<A> = FunctionName<A>,
85
+ T extends TransformKey = "candid",
86
+ > = Omit<UseActorMethodParameters<A, M, T>, "reactor">
87
+
88
+ /**
89
+ * Result type for useActorMethod hook.
90
+ * Provides a unified interface for both query and mutation methods.
91
+ */
92
+ export interface UseActorMethodResult<
93
+ A = BaseActor,
94
+ M extends FunctionName<A> = FunctionName<A>,
95
+ T extends TransformKey = "candid",
96
+ > {
97
+ /** The returned data from the method call */
98
+ data: ReactorReturnOk<A, M, T> | undefined
99
+
100
+ /** Whether the method is currently executing */
101
+ isLoading: boolean
102
+
103
+ /** Alias for isLoading - whether a mutation is pending */
104
+ isPending: boolean
105
+
106
+ /** Whether there was an error */
107
+ isError: boolean
108
+
109
+ /** Whether the method has successfully completed at least once */
110
+ isSuccess: boolean
111
+
112
+ /** The error if one occurred */
113
+ error: ReactorReturnErr<A, M, T> | null
114
+
115
+ /** Whether this is a query method (true) or mutation method (false) */
116
+ isQuery: boolean
117
+
118
+ /** The function type (query, update, composite_query) */
119
+ functionType: FunctionType
120
+
121
+ /**
122
+ * Call the method with optional arguments.
123
+ * For queries: triggers a refetch
124
+ * For mutations: executes the mutation with the provided args
125
+ */
126
+ call: (
127
+ args?: ReactorArgs<A, M, T>
128
+ ) => Promise<ReactorReturnOk<A, M, T> | undefined>
129
+
130
+ /**
131
+ * Reset the state (clear data and error).
132
+ * For queries: removes the query from cache
133
+ * For mutations: resets the mutation state
134
+ */
135
+ reset: () => void
136
+
137
+ /**
138
+ * For queries only: Refetch the query
139
+ */
140
+ refetch: () => Promise<ReactorReturnOk<A, M, T> | undefined>
141
+
142
+ // Expose underlying results for advanced use cases
143
+ /** The raw query result (only available for query methods) */
144
+ queryResult?: UseQueryResult<
145
+ ReactorReturnOk<A, M, T>,
146
+ ReactorReturnErr<A, M, T>
147
+ >
148
+
149
+ /** The raw mutation result (only available for mutation methods) */
150
+ mutationResult?: UseMutationResult<
151
+ ReactorReturnOk<A, M, T>,
152
+ ReactorReturnErr<A, M, T>,
153
+ ReactorArgs<A, M, T>
154
+ >
155
+ }
156
+
157
+ /**
158
+ * A unified hook for calling canister methods that automatically handles
159
+ * both query and mutation methods based on the Candid interface.
160
+ */
161
+ export function useActorMethod<
162
+ A = BaseActor,
163
+ M extends FunctionName<A> = FunctionName<A>,
164
+ T extends TransformKey = "candid",
165
+ >({
166
+ reactor,
167
+ functionName,
168
+ args,
169
+ callConfig,
170
+ queryKey: customQueryKey,
171
+ enabled = true,
172
+ onSuccess,
173
+ onError,
174
+ invalidateQueries,
175
+ ...queryOptions
176
+ }: UseActorMethodParameters<A, M, T>): UseActorMethodResult<A, M, T> {
177
+ // Determine if this is a query method by checking the IDL
178
+ const isQuery = useMemo(() => {
179
+ if (!reactor) throw new Error("Reactor instance is required")
180
+ return reactor.isQueryMethod(functionName)
181
+ }, [reactor, functionName])
182
+
183
+ const functionType: FunctionType = isQuery ? "query" : "update"
184
+
185
+ // Generate query key
186
+ const queryKey = useMemo(() => {
187
+ if (customQueryKey) return customQueryKey
188
+ return reactor.generateQueryKey({
189
+ functionName,
190
+ args,
191
+ })
192
+ }, [reactor, functionName, args, customQueryKey])
193
+
194
+ // ============================================================================
195
+ // Query Implementation
196
+ // ============================================================================
197
+
198
+ const queryResult = useQuery<
199
+ ReactorReturnOk<A, M, T>,
200
+ ReactorReturnErr<A, M, T>
201
+ >(
202
+ {
203
+ queryKey,
204
+ queryFn: async () => {
205
+ try {
206
+ const result = await reactor.callMethod({
207
+ functionName,
208
+ args,
209
+ callConfig,
210
+ })
211
+ onSuccess?.(result)
212
+ return result
213
+ } catch (error) {
214
+ onError?.(error as ReactorReturnErr<A, M, T>)
215
+ throw error
216
+ }
217
+ },
218
+ enabled: isQuery && enabled,
219
+ ...queryOptions,
220
+ },
221
+ reactor.queryClient
222
+ )
223
+
224
+ // ============================================================================
225
+ // Mutation Implementation
226
+ // ============================================================================
227
+
228
+ const mutationResult = useMutation<
229
+ ReactorReturnOk<A, M, T>,
230
+ ReactorReturnErr<A, M, T>,
231
+ ReactorArgs<A, M, T>
232
+ >(
233
+ {
234
+ mutationKey: queryKey,
235
+ mutationFn: async (mutationArgs) => {
236
+ const result = await reactor.callMethod({
237
+ functionName,
238
+ args: mutationArgs ?? args,
239
+ callConfig,
240
+ })
241
+ return result
242
+ },
243
+ onSuccess: (data) => {
244
+ onSuccess?.(data)
245
+ // Invalidate specified queries after successful mutation
246
+ if (invalidateQueries && invalidateQueries.length > 0) {
247
+ invalidateQueries.forEach((key) => {
248
+ reactor.queryClient.invalidateQueries({ queryKey: key })
249
+ })
250
+ }
251
+ },
252
+ onError: (error) => {
253
+ onError?.(error)
254
+ },
255
+ },
256
+ reactor.queryClient
257
+ )
258
+
259
+ // ============================================================================
260
+ // Unified Call Function
261
+ // ============================================================================
262
+
263
+ const call = useCallback(
264
+ async (
265
+ callArgs?: ReactorArgs<A, M, T>
266
+ ): Promise<ReactorReturnOk<A, M, T> | undefined> => {
267
+ if (isQuery) {
268
+ // For queries, refetch with new args if provided
269
+ if (callArgs !== undefined) {
270
+ try {
271
+ const result = await reactor.queryClient.fetchQuery({
272
+ queryKey,
273
+ queryFn: () =>
274
+ reactor.callMethod({
275
+ functionName,
276
+ args: callArgs,
277
+ callConfig,
278
+ }),
279
+ staleTime: 0,
280
+ })
281
+ onSuccess?.(result)
282
+ return result
283
+ } catch (error) {
284
+ onError?.(error as ReactorReturnErr<A, M, T>)
285
+ return undefined
286
+ }
287
+ }
288
+ // Otherwise just refetch
289
+ const { data } = await queryResult.refetch()
290
+ return data
291
+ } else {
292
+ // For mutations, execute with provided args
293
+ return mutationResult
294
+ .mutateAsync(callArgs as ReactorArgs<A, M, T>)
295
+ .catch(() => undefined)
296
+ }
297
+ },
298
+ [
299
+ isQuery,
300
+ reactor,
301
+ functionName,
302
+ callConfig,
303
+ queryKey,
304
+ queryResult,
305
+ mutationResult,
306
+ onSuccess,
307
+ onError,
308
+ ]
309
+ )
310
+
311
+ // ============================================================================
312
+ // Reset Function
313
+ // ============================================================================
314
+
315
+ const reset = useCallback(() => {
316
+ if (isQuery) {
317
+ reactor.queryClient.removeQueries({ queryKey })
318
+ } else {
319
+ mutationResult.reset()
320
+ }
321
+ }, [isQuery, reactor, queryKey, mutationResult])
322
+
323
+ // ============================================================================
324
+ // Refetch Function
325
+ // ============================================================================
326
+
327
+ const refetch = useCallback(async () => {
328
+ if (isQuery) {
329
+ const result = await queryResult.refetch()
330
+ return result.data
331
+ }
332
+ return undefined
333
+ }, [isQuery, queryResult])
334
+
335
+ // ============================================================================
336
+ // Return Unified Result
337
+ // ============================================================================
338
+
339
+ if (isQuery) {
340
+ return {
341
+ data: queryResult.data,
342
+ isLoading: queryResult.isLoading,
343
+ isPending: queryResult.isLoading,
344
+ isError: queryResult.isError,
345
+ isSuccess: queryResult.isSuccess,
346
+ error: queryResult.error,
347
+ isQuery: true,
348
+ functionType,
349
+ call,
350
+ reset,
351
+ refetch,
352
+ queryResult,
353
+ } as UseActorMethodResult<A, M, T>
354
+ } else {
355
+ return {
356
+ data: mutationResult.data,
357
+ isLoading: mutationResult.isPending,
358
+ isPending: mutationResult.isPending,
359
+ isError: mutationResult.isError,
360
+ isSuccess: mutationResult.isSuccess,
361
+ error: mutationResult.error,
362
+ isQuery: false,
363
+ functionType,
364
+ call,
365
+ reset,
366
+ refetch,
367
+ mutationResult,
368
+ } as UseActorMethodResult<A, M, T>
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Creates a bound useMethod hook for a specific reactor instance.
374
+ *
375
+ * @example
376
+ * ```tsx
377
+ * const { useMethod } = createActorMethodHooks(reactor)
378
+ * ```
379
+ */
380
+ export function createActorMethodHooks<
381
+ A = BaseActor,
382
+ T extends TransformKey = "candid",
383
+ >(reactor: Reactor<A, T>) {
384
+ return {
385
+ /**
386
+ * Hook for calling methods on the bound reactor.
387
+ */
388
+ useMethod: <M extends FunctionName<A>>(
389
+ config: Omit<UseActorMethodParameters<A, M, T>, "reactor">
390
+ ) =>
391
+ useActorMethod({ ...config, reactor } as UseActorMethodParameters<
392
+ A,
393
+ M,
394
+ T
395
+ >),
396
+ }
397
+ }
@@ -0,0 +1,220 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest"
2
+ import { renderHook } from "@testing-library/react"
3
+ import React from "react"
4
+ import { ClientManager, Reactor } from "@ic-reactor/core"
5
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
6
+ import { Actor, ActorMethod } from "@icp-sdk/core/agent"
7
+ import { useActorMutation } from "./useActorMutation"
8
+
9
+ // Mock IDL factory
10
+ const idlFactory = ({ IDL }: any) => {
11
+ return IDL.Service({
12
+ create_user: IDL.Func(
13
+ [IDL.Record({ name: IDL.Text })],
14
+ [IDL.Record({ id: IDL.Text, name: IDL.Text })],
15
+ ["update"]
16
+ ),
17
+ update_user: IDL.Func(
18
+ [IDL.Record({ id: IDL.Text, name: IDL.Text })],
19
+ [IDL.Record({ id: IDL.Text, name: IDL.Text })],
20
+ ["update"]
21
+ ),
22
+ delete_user: IDL.Func([IDL.Text], [IDL.Bool], ["update"]),
23
+ })
24
+ }
25
+
26
+ // Define Actor Interface
27
+ interface CreateUserInput {
28
+ name: string
29
+ }
30
+
31
+ interface UpdateUserInput {
32
+ id: string
33
+ name: string
34
+ }
35
+
36
+ interface User {
37
+ id: string
38
+ name: string
39
+ }
40
+
41
+ // Actor type with methods
42
+ interface TestActor {
43
+ get_user: ActorMethod<[string], User>
44
+ create_user: ActorMethod<[CreateUserInput], User>
45
+ update_user: ActorMethod<[UpdateUserInput], User>
46
+ delete_user: ActorMethod<[string], boolean>
47
+ }
48
+
49
+ // Mock data
50
+ const mockUserCreated: User = { id: "user-1", name: "Alice" }
51
+ const mockUserUpdated: User = { id: "user-1", name: "Bob" }
52
+
53
+ // Mock Actor.createActor
54
+ vi.mock("@icp-sdk/core/agent", async () => {
55
+ const actual = await vi.importActual<typeof import("@icp-sdk/core/agent")>(
56
+ "@icp-sdk/core/agent"
57
+ )
58
+ return {
59
+ ...actual,
60
+ Actor: class extends actual.Actor {
61
+ static createActor = vi.fn()
62
+ static canisterIdOf = vi.fn().mockReturnValue({
63
+ toString: () => "test-canister-id",
64
+ })
65
+ static interfaceOf = vi.fn().mockReturnValue({
66
+ _fields: [],
67
+ })
68
+ },
69
+ }
70
+ })
71
+
72
+ describe("useActorMutation", () => {
73
+ let queryClient: QueryClient
74
+ let clientManager: ClientManager
75
+ let mockActor: TestActor
76
+ let reactor: Reactor<TestActor>
77
+
78
+ beforeEach(() => {
79
+ queryClient = new QueryClient({
80
+ defaultOptions: {
81
+ mutations: {
82
+ retry: false,
83
+ },
84
+ },
85
+ })
86
+
87
+ clientManager = new ClientManager({ queryClient })
88
+
89
+ // Setup mock actor
90
+ mockActor = {
91
+ create_user: vi.fn().mockResolvedValue(mockUserCreated),
92
+ update_user: vi.fn().mockResolvedValue(mockUserUpdated),
93
+ delete_user: vi.fn().mockResolvedValue(true),
94
+ } as unknown as TestActor
95
+ ;(Actor.createActor as any).mockReturnValue(mockActor)
96
+
97
+ reactor = new Reactor<TestActor>({
98
+ clientManager,
99
+ canisterId: "rrkah-fqaaa-aaaaa-aaaaq-cai",
100
+ idlFactory,
101
+ name: "test",
102
+ })
103
+ })
104
+
105
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
106
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
107
+ )
108
+
109
+ describe("basic functionality", () => {
110
+ it("should create a mutation hook", () => {
111
+ const { result } = renderHook(
112
+ () =>
113
+ useActorMutation({
114
+ reactor,
115
+ functionName: "create_user",
116
+ }),
117
+ { wrapper }
118
+ )
119
+
120
+ expect(result.current).toBeDefined()
121
+ })
122
+
123
+ it("should return correct hook structure", () => {
124
+ const { result } = renderHook(
125
+ () =>
126
+ useActorMutation({
127
+ reactor,
128
+ functionName: "create_user",
129
+ }),
130
+ { wrapper }
131
+ )
132
+
133
+ expect(result.current.mutate).toBeInstanceOf(Function)
134
+ expect(result.current.mutateAsync).toBeInstanceOf(Function)
135
+ expect(typeof result.current.isPending).toBe("boolean")
136
+ expect(typeof result.current.isSuccess).toBe("boolean")
137
+ expect(typeof result.current.isError).toBe("boolean")
138
+ })
139
+
140
+ it("should have correct initial state", () => {
141
+ const { result } = renderHook(
142
+ () =>
143
+ useActorMutation({
144
+ reactor,
145
+ functionName: "create_user",
146
+ }),
147
+ { wrapper }
148
+ )
149
+
150
+ expect(result.current.isPending).toBe(false)
151
+ expect(result.current.isSuccess).toBe(false)
152
+ expect(result.current.isError).toBe(false)
153
+ expect(result.current.data).toBeUndefined()
154
+ })
155
+ })
156
+
157
+ describe("hook options", () => {
158
+ it("should accept mutation options like onSuccess and onError", () => {
159
+ const onSuccess = vi.fn()
160
+ const onError = vi.fn()
161
+
162
+ const { result } = renderHook(
163
+ () =>
164
+ useActorMutation({
165
+ reactor,
166
+ functionName: "create_user",
167
+ onSuccess,
168
+ onError,
169
+ }),
170
+ { wrapper }
171
+ )
172
+
173
+ expect(result.current.mutate).toBeInstanceOf(Function)
174
+ })
175
+
176
+ it("should expose mutate and mutateAsync methods", () => {
177
+ const { result } = renderHook(
178
+ () =>
179
+ useActorMutation({
180
+ reactor,
181
+ functionName: "create_user",
182
+ }),
183
+ { wrapper }
184
+ )
185
+
186
+ expect(result.current.mutate).toBeInstanceOf(Function)
187
+ expect(result.current.mutateAsync).toBeInstanceOf(Function)
188
+ })
189
+ })
190
+
191
+ describe("invalidateQueries functionality", () => {
192
+ it("should accept invalidateQueries option", () => {
193
+ const { result } = renderHook(
194
+ () =>
195
+ useActorMutation({
196
+ reactor,
197
+ functionName: "update_user",
198
+ invalidateQueries: [["test-canister", "get_user"]],
199
+ }),
200
+ { wrapper }
201
+ )
202
+
203
+ expect(result.current.mutate).toBeInstanceOf(Function)
204
+ })
205
+
206
+ it("should support invalidateQueries with args", () => {
207
+ const { result } = renderHook(
208
+ () =>
209
+ useActorMutation({
210
+ reactor,
211
+ functionName: "update_user",
212
+ invalidateQueries: [["test-canister", "get_user", "user-1"]],
213
+ }),
214
+ { wrapper }
215
+ )
216
+
217
+ expect(result.current.mutate).toBeInstanceOf(Function)
218
+ })
219
+ })
220
+ })