@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,124 @@
1
+ import { useMemo, useCallback } from "react"
2
+ import {
3
+ useMutation,
4
+ UseMutationOptions,
5
+ UseMutationResult,
6
+ QueryKey,
7
+ } from "@tanstack/react-query"
8
+ import {
9
+ Reactor,
10
+ ReactorArgs,
11
+ ReactorReturnOk,
12
+ FunctionName,
13
+ TransformKey,
14
+ ReactorReturnErr,
15
+ } from "@ic-reactor/core"
16
+ import { CallConfig } from "@icp-sdk/core/agent"
17
+
18
+ export interface UseActorMutationParameters<
19
+ A,
20
+ M extends FunctionName<A>,
21
+ T extends TransformKey = "candid",
22
+ > extends Omit<
23
+ UseMutationOptions<
24
+ ReactorReturnOk<A, M, T>,
25
+ ReactorReturnErr<A, M, T>,
26
+ ReactorArgs<A, M, T>
27
+ >,
28
+ "mutationFn"
29
+ > {
30
+ reactor: Reactor<A, T>
31
+ functionName: M
32
+ callConfig?: CallConfig
33
+ invalidateQueries?: QueryKey[]
34
+ }
35
+
36
+ export type UseActorMutationConfig<
37
+ A,
38
+ M extends FunctionName<A>,
39
+ T extends TransformKey = "candid",
40
+ > = Omit<UseActorMutationParameters<A, M, T>, "reactor">
41
+
42
+ export type UseActorMutationResult<
43
+ A,
44
+ M extends FunctionName<A>,
45
+ T extends TransformKey = "candid",
46
+ > = UseMutationResult<
47
+ ReactorReturnOk<A, M, T>,
48
+ ReactorReturnErr<A, M, T>,
49
+ ReactorArgs<A, M, T>
50
+ >
51
+
52
+ /**
53
+ * Hook for executing mutation calls on a canister.
54
+ *
55
+ * @example
56
+ * const { mutate, isPending } = useActorMutation({
57
+ * reactor,
58
+ * functionName: "transfer",
59
+ * onSuccess: () => console.log("Success!"),
60
+ * })
61
+ */
62
+ export const useActorMutation = <
63
+ A,
64
+ M extends FunctionName<A>,
65
+ T extends TransformKey = "candid",
66
+ >({
67
+ reactor,
68
+ functionName,
69
+ invalidateQueries,
70
+ onSuccess,
71
+ callConfig,
72
+ ...options
73
+ }: UseActorMutationParameters<A, M, T>): UseActorMutationResult<A, M, T> => {
74
+ // Memoize mutationFn to avoid creating new function on every render
75
+ const mutationFn = useCallback(
76
+ async (args: ReactorArgs<A, M, T>) => {
77
+ return reactor.callMethod({
78
+ functionName,
79
+ callConfig,
80
+ args,
81
+ })
82
+ },
83
+ [reactor, functionName, callConfig]
84
+ )
85
+
86
+ // Memoize onSuccess handler
87
+ const handleSuccess = useCallback(
88
+ async (
89
+ ...params: Parameters<
90
+ NonNullable<
91
+ UseMutationOptions<
92
+ ReactorReturnOk<A, M, T>,
93
+ ReactorReturnErr<A, M, T>,
94
+ ReactorArgs<A, M, T>
95
+ >["onSuccess"]
96
+ >
97
+ >
98
+ ) => {
99
+ if (invalidateQueries) {
100
+ await Promise.all(
101
+ invalidateQueries.map((queryKey) =>
102
+ reactor.queryClient.invalidateQueries({ queryKey })
103
+ )
104
+ )
105
+ }
106
+ if (onSuccess) {
107
+ await onSuccess(...params)
108
+ }
109
+ },
110
+ [reactor, invalidateQueries, onSuccess]
111
+ )
112
+
113
+ // Memoize mutation options
114
+ const mutationOptions = useMemo(
115
+ () => ({
116
+ ...options,
117
+ mutationFn,
118
+ onSuccess: handleSuccess,
119
+ }),
120
+ [options, mutationFn, handleSuccess]
121
+ )
122
+
123
+ return useMutation(mutationOptions, reactor.queryClient)
124
+ }
@@ -0,0 +1,287 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest"
2
+ import { renderHook, waitFor } from "@testing-library/react"
3
+ import React from "react"
4
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
5
+ import { useActorQuery } from "./useActorQuery"
6
+ import { ActorMethod } from "@icp-sdk/core/agent"
7
+ import { Reactor } from "@ic-reactor/core"
8
+
9
+ // Define a test actor type
10
+ type TestActor = {
11
+ greet: ActorMethod<[], string>
12
+ greetWithName: ActorMethod<[string], string>
13
+ getCount: ActorMethod<[], bigint>
14
+ }
15
+
16
+ // Mock Reactor
17
+ const createMockReactor = (queryClient: QueryClient): Reactor<TestActor> =>
18
+ ({
19
+ queryClient,
20
+ getQueryOptions: vi.fn().mockImplementation(({ functionName, args }) => ({
21
+ queryKey: ["test-canister", functionName, ...(args || [])],
22
+ queryFn: vi.fn().mockImplementation(async () => {
23
+ if (functionName === "greet") return "Hello, World!"
24
+ if (functionName === "greetWithName")
25
+ return `Hello, ${args?.[0] || "Guest"}!`
26
+ if (functionName === "getCount") return BigInt(42)
27
+ return null
28
+ }),
29
+ })),
30
+ }) as unknown as Reactor<TestActor>
31
+
32
+ describe("useActorQuery", () => {
33
+ let queryClient: QueryClient
34
+ let mockReactor: Reactor<TestActor>
35
+
36
+ beforeEach(() => {
37
+ queryClient = new QueryClient({
38
+ defaultOptions: {
39
+ queries: {
40
+ retry: false,
41
+ },
42
+ },
43
+ })
44
+ mockReactor = createMockReactor(queryClient)
45
+ })
46
+
47
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
48
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
49
+ )
50
+
51
+ describe("basic functionality", () => {
52
+ it("should fetch data without select", async () => {
53
+ const { result } = renderHook(
54
+ () =>
55
+ useActorQuery<TestActor, "greet">({
56
+ reactor: mockReactor,
57
+ functionName: "greet",
58
+ }),
59
+ { wrapper }
60
+ )
61
+
62
+ await waitFor(() => {
63
+ expect(result.current.isSuccess).toBe(true)
64
+ })
65
+
66
+ expect(result.current.data).toBe("Hello, World!")
67
+ })
68
+
69
+ it("should pass args to the query", async () => {
70
+ const { result } = renderHook(
71
+ () =>
72
+ useActorQuery<TestActor, "greetWithName">({
73
+ reactor: mockReactor,
74
+ functionName: "greetWithName",
75
+ args: ["Alice"],
76
+ }),
77
+ { wrapper }
78
+ )
79
+
80
+ await waitFor(() => {
81
+ expect(result.current.isSuccess).toBe(true)
82
+ })
83
+
84
+ expect(result.current.data).toBe("Hello, Alice!")
85
+ expect(mockReactor.getQueryOptions).toHaveBeenCalledWith({
86
+ callConfig: undefined,
87
+ functionName: "greetWithName",
88
+ args: ["Alice"],
89
+ })
90
+ })
91
+
92
+ it("should handle bigint return types", async () => {
93
+ const { result } = renderHook(
94
+ () =>
95
+ useActorQuery<TestActor, "getCount">({
96
+ reactor: mockReactor,
97
+ functionName: "getCount",
98
+ }),
99
+ { wrapper }
100
+ )
101
+
102
+ await waitFor(() => {
103
+ expect(result.current.isSuccess).toBe(true)
104
+ })
105
+
106
+ expect(result.current.data).toBe(BigInt(42))
107
+ })
108
+ })
109
+
110
+ describe("select function", () => {
111
+ it("should transform data with select function", async () => {
112
+ const { result } = renderHook(
113
+ () =>
114
+ useActorQuery<TestActor, "greet", "candid", number>({
115
+ reactor: mockReactor,
116
+ functionName: "greet",
117
+ select: (data: string) => data.length,
118
+ }),
119
+ { wrapper }
120
+ )
121
+
122
+ await waitFor(() => {
123
+ expect(result.current.isSuccess).toBe(true)
124
+ })
125
+
126
+ // "Hello, World!" has 13 characters
127
+ expect(result.current.data).toBe(13)
128
+ })
129
+
130
+ it("should transform data to different type with select", async () => {
131
+ const { result } = renderHook(
132
+ () =>
133
+ useActorQuery<TestActor, "greet", "candid", { message: string }>({
134
+ reactor: mockReactor,
135
+ functionName: "greet",
136
+ select: (data: string) => ({ message: data }),
137
+ }),
138
+ { wrapper }
139
+ )
140
+
141
+ await waitFor(() => {
142
+ expect(result.current.isSuccess).toBe(true)
143
+ })
144
+
145
+ expect(result.current.data).toEqual({ message: "Hello, World!" })
146
+ })
147
+
148
+ it("should transform bigint to number with select", async () => {
149
+ const { result } = renderHook(
150
+ () =>
151
+ useActorQuery<TestActor, "getCount", "candid", number>({
152
+ reactor: mockReactor,
153
+ functionName: "getCount",
154
+ select: (data: bigint) => Number(data),
155
+ }),
156
+ { wrapper }
157
+ )
158
+
159
+ await waitFor(() => {
160
+ expect(result.current.isSuccess).toBe(true)
161
+ })
162
+
163
+ expect(result.current.data).toBe(42)
164
+ expect(typeof result.current.data).toBe("number")
165
+ })
166
+
167
+ it("should work with array transformation in select", async () => {
168
+ const { result } = renderHook(
169
+ () =>
170
+ useActorQuery<TestActor, "greet", "candid", string[]>({
171
+ reactor: mockReactor,
172
+ functionName: "greet",
173
+ select: (data: string) => data.split(", "),
174
+ }),
175
+ { wrapper }
176
+ )
177
+
178
+ await waitFor(() => {
179
+ expect(result.current.isSuccess).toBe(true)
180
+ })
181
+
182
+ expect(result.current.data).toEqual(["Hello", "World!"])
183
+ })
184
+ })
185
+
186
+ describe("query options", () => {
187
+ it("should respect enabled: false option", async () => {
188
+ const { result } = renderHook(
189
+ () =>
190
+ useActorQuery<TestActor, "greet">({
191
+ reactor: mockReactor,
192
+ functionName: "greet",
193
+ enabled: false,
194
+ }),
195
+ { wrapper }
196
+ )
197
+
198
+ // Should stay in pending state
199
+ expect(result.current.isPending).toBe(true)
200
+ expect(result.current.fetchStatus).toBe("idle")
201
+ expect(result.current.data).toBeUndefined()
202
+ })
203
+
204
+ it("should use custom staleTime", async () => {
205
+ const { result } = renderHook(
206
+ () =>
207
+ useActorQuery<TestActor, "greet">({
208
+ reactor: mockReactor,
209
+ functionName: "greet",
210
+ staleTime: 1000 * 60 * 10, // 10 minutes
211
+ }),
212
+ { wrapper }
213
+ )
214
+
215
+ await waitFor(() => {
216
+ expect(result.current.isSuccess).toBe(true)
217
+ })
218
+
219
+ expect(result.current.isStale).toBe(false)
220
+ })
221
+ })
222
+
223
+ describe("type inference", () => {
224
+ it("should infer string type for greet method without select", async () => {
225
+ const { result } = renderHook(
226
+ () =>
227
+ useActorQuery<TestActor, "greet">({
228
+ reactor: mockReactor,
229
+ functionName: "greet",
230
+ }),
231
+ { wrapper }
232
+ )
233
+
234
+ await waitFor(() => {
235
+ expect(result.current.isSuccess).toBe(true)
236
+ })
237
+
238
+ // TypeScript should infer data as string
239
+ const data: string | undefined = result.current.data
240
+ expect(typeof data).toBe("string")
241
+ })
242
+
243
+ it("should infer number type when select returns number", async () => {
244
+ const { result } = renderHook(
245
+ () =>
246
+ useActorQuery({
247
+ reactor: mockReactor,
248
+ functionName: "greet",
249
+ select: (data: any) => data.length,
250
+ }),
251
+ { wrapper }
252
+ )
253
+
254
+ await waitFor(() => {
255
+ expect(result.current.isSuccess).toBe(true)
256
+ })
257
+
258
+ // TypeScript should infer data as number
259
+ const data: number | undefined = result.current.data
260
+ expect(typeof data).toBe("number")
261
+ })
262
+ })
263
+
264
+ describe("refetching", () => {
265
+ it("should support manual refetch", async () => {
266
+ const { result } = renderHook(
267
+ () =>
268
+ useActorQuery<TestActor, "greet">({
269
+ reactor: mockReactor,
270
+ functionName: "greet",
271
+ }),
272
+ { wrapper }
273
+ )
274
+
275
+ await waitFor(() => {
276
+ expect(result.current.isSuccess).toBe(true)
277
+ })
278
+
279
+ // Refetch should be available
280
+ expect(typeof result.current.refetch).toBe("function")
281
+
282
+ await result.current.refetch()
283
+
284
+ expect(result.current.isSuccess).toBe(true)
285
+ })
286
+ })
287
+ })
@@ -0,0 +1,110 @@
1
+ import { useMemo } from "react"
2
+ import {
3
+ QueryKey,
4
+ useQuery,
5
+ QueryObserverOptions,
6
+ UseQueryResult,
7
+ } from "@tanstack/react-query"
8
+ import {
9
+ FunctionName,
10
+ Reactor,
11
+ TransformKey,
12
+ ReactorArgs,
13
+ ReactorReturnOk,
14
+ ReactorReturnErr,
15
+ } from "@ic-reactor/core"
16
+ import { CallConfig } from "@icp-sdk/core/agent"
17
+
18
+ export interface UseActorQueryParameters<
19
+ A,
20
+ M extends FunctionName<A>,
21
+ T extends TransformKey = "candid",
22
+ TSelected = ReactorReturnOk<A, M, T>,
23
+ > extends Omit<
24
+ QueryObserverOptions<
25
+ ReactorReturnOk<A, M, T>,
26
+ ReactorReturnErr<A, M, T>,
27
+ TSelected,
28
+ ReactorReturnOk<A, M, T>,
29
+ QueryKey
30
+ >,
31
+ "queryKey" | "queryFn"
32
+ > {
33
+ reactor: Reactor<A, T>
34
+ functionName: M
35
+ args?: ReactorArgs<A, M, T>
36
+ callConfig?: CallConfig
37
+ queryKey?: QueryKey
38
+ }
39
+
40
+ export type UseActorQueryConfig<
41
+ A,
42
+ M extends FunctionName<A>,
43
+ T extends TransformKey = "candid",
44
+ TSelected = ReactorReturnOk<A, M, T>,
45
+ > = Omit<UseActorQueryParameters<A, M, T, TSelected>, "reactor">
46
+
47
+ export type UseActorQueryResult<
48
+ A,
49
+ M extends FunctionName<A>,
50
+ T extends TransformKey = "candid",
51
+ TSelected = ReactorReturnOk<A, M, T>,
52
+ > = UseQueryResult<TSelected, ReactorReturnErr<A, M, T>>
53
+
54
+ /**
55
+ * Hook for executing query calls on a canister.
56
+ *
57
+ * @example
58
+ * const { data, isLoading } = useActorQuery({
59
+ * reactor,
60
+ * functionName: "getUser",
61
+ * args: ["user-123"],
62
+ * })
63
+ *
64
+ * // With select transformation
65
+ * const { data } = useActorQuery({
66
+ * reactor,
67
+ * functionName: "getUser",
68
+ * args: ["user-123"],
69
+ * select: (user) => user.name,
70
+ * })
71
+ */
72
+ export const useActorQuery = <
73
+ A,
74
+ M extends FunctionName<A>,
75
+ T extends TransformKey = "candid",
76
+ TSelected = ReactorReturnOk<A, M, T>,
77
+ >({
78
+ reactor,
79
+ functionName,
80
+ args,
81
+ callConfig,
82
+ queryKey: defaultQueryKey,
83
+ ...options
84
+ }: UseActorQueryParameters<A, M, T, TSelected>): UseActorQueryResult<
85
+ A,
86
+ M,
87
+ T,
88
+ TSelected
89
+ > => {
90
+ // Memoize query options to prevent unnecessary re-computations
91
+ const { queryKey, queryFn } = useMemo(
92
+ () =>
93
+ reactor.getQueryOptions<M>({
94
+ callConfig,
95
+ functionName,
96
+ args,
97
+ queryKey: defaultQueryKey,
98
+ }),
99
+ [reactor, callConfig, functionName, args, defaultQueryKey]
100
+ )
101
+
102
+ return useQuery(
103
+ {
104
+ queryFn,
105
+ ...options,
106
+ queryKey,
107
+ },
108
+ reactor.queryClient
109
+ )
110
+ }