@ic-reactor/react 3.0.0-beta.7 → 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 +9 -8
  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,254 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest"
2
+ import { renderHook, waitFor } from "@testing-library/react"
3
+ import React, { Suspense } from "react"
4
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
5
+ import { useActorSuspenseQuery } from "./useActorSuspenseQuery"
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("useActorSuspenseQuery", () => {
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}>
49
+ <Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
50
+ </QueryClientProvider>
51
+ )
52
+
53
+ describe("basic functionality", () => {
54
+ it("should fetch data with suspense", async () => {
55
+ const { result } = renderHook(
56
+ () =>
57
+ useActorSuspenseQuery<TestActor, "greet">({
58
+ reactor: mockReactor,
59
+ functionName: "greet",
60
+ }),
61
+ { wrapper }
62
+ )
63
+
64
+ await waitFor(() => {
65
+ expect(result.current.isSuccess).toBe(true)
66
+ })
67
+
68
+ expect(result.current.data).toBe("Hello, World!")
69
+ })
70
+
71
+ it("should pass args to the query", async () => {
72
+ const { result } = renderHook(
73
+ () =>
74
+ useActorSuspenseQuery<TestActor, "greetWithName">({
75
+ reactor: mockReactor,
76
+ functionName: "greetWithName",
77
+ args: ["Alice"],
78
+ }),
79
+ { wrapper }
80
+ )
81
+
82
+ await waitFor(() => {
83
+ expect(result.current.isSuccess).toBe(true)
84
+ })
85
+
86
+ expect(result.current.data).toBe("Hello, Alice!")
87
+ expect(mockReactor.getQueryOptions).toHaveBeenCalledWith({
88
+ callConfig: undefined,
89
+ functionName: "greetWithName",
90
+ args: ["Alice"],
91
+ })
92
+ })
93
+
94
+ it("should handle bigint return types", async () => {
95
+ const { result } = renderHook(
96
+ () =>
97
+ useActorSuspenseQuery<TestActor, "getCount">({
98
+ reactor: mockReactor,
99
+ functionName: "getCount",
100
+ }),
101
+ { wrapper }
102
+ )
103
+
104
+ await waitFor(() => {
105
+ expect(result.current.isSuccess).toBe(true)
106
+ })
107
+
108
+ expect(result.current.data).toBe(BigInt(42))
109
+ })
110
+ })
111
+
112
+ describe("select function", () => {
113
+ it("should transform data with select function", async () => {
114
+ const { result } = renderHook(
115
+ () =>
116
+ useActorSuspenseQuery<TestActor, "greet", "candid", number>({
117
+ reactor: mockReactor,
118
+ functionName: "greet",
119
+ select: (data: string) => data.length,
120
+ }),
121
+ { wrapper }
122
+ )
123
+
124
+ await waitFor(() => {
125
+ expect(result.current.isSuccess).toBe(true)
126
+ })
127
+
128
+ // "Hello, World!" has 13 characters
129
+ expect(result.current.data).toBe(13)
130
+ })
131
+
132
+ it("should transform data to different type with select", async () => {
133
+ const { result } = renderHook(
134
+ () =>
135
+ useActorSuspenseQuery<
136
+ TestActor,
137
+ "greet",
138
+ "candid",
139
+ { message: string }
140
+ >({
141
+ reactor: mockReactor,
142
+ functionName: "greet",
143
+ select: (data: string) => ({ message: data }),
144
+ }),
145
+ { wrapper }
146
+ )
147
+
148
+ await waitFor(() => {
149
+ expect(result.current.isSuccess).toBe(true)
150
+ })
151
+
152
+ expect(result.current.data).toEqual({ message: "Hello, World!" })
153
+ })
154
+
155
+ it("should transform bigint to number with select", async () => {
156
+ const { result } = renderHook(
157
+ () =>
158
+ useActorSuspenseQuery<TestActor, "getCount", "candid", number>({
159
+ reactor: mockReactor,
160
+ functionName: "getCount",
161
+ select: (data: bigint) => Number(data),
162
+ }),
163
+ { wrapper }
164
+ )
165
+
166
+ await waitFor(() => {
167
+ expect(result.current.isSuccess).toBe(true)
168
+ })
169
+
170
+ expect(result.current.data).toBe(42)
171
+ expect(typeof result.current.data).toBe("number")
172
+ })
173
+ })
174
+
175
+ describe("query options", () => {
176
+ it("should use custom staleTime", async () => {
177
+ const { result } = renderHook(
178
+ () =>
179
+ useActorSuspenseQuery<TestActor, "greet">({
180
+ reactor: mockReactor,
181
+ functionName: "greet",
182
+ staleTime: 1000 * 60 * 10, // 10 minutes
183
+ }),
184
+ { wrapper }
185
+ )
186
+
187
+ await waitFor(() => {
188
+ expect(result.current.isSuccess).toBe(true)
189
+ })
190
+
191
+ expect(result.current.isStale).toBe(false)
192
+ })
193
+ })
194
+
195
+ describe("suspense behavior", () => {
196
+ it("should always return data (no undefined) when successful", async () => {
197
+ const { result } = renderHook(
198
+ () =>
199
+ useActorSuspenseQuery<TestActor, "greet">({
200
+ reactor: mockReactor,
201
+ functionName: "greet",
202
+ }),
203
+ { wrapper }
204
+ )
205
+
206
+ await waitFor(() => {
207
+ expect(result.current.isSuccess).toBe(true)
208
+ })
209
+
210
+ // Suspense queries always have data when rendered
211
+ expect(result.current.data).toBeDefined()
212
+ expect(result.current.data).toBe("Hello, World!")
213
+ })
214
+
215
+ it("should have status 'success' when data is available", async () => {
216
+ const { result } = renderHook(
217
+ () =>
218
+ useActorSuspenseQuery<TestActor, "greet">({
219
+ reactor: mockReactor,
220
+ functionName: "greet",
221
+ }),
222
+ { wrapper }
223
+ )
224
+
225
+ await waitFor(() => {
226
+ expect(result.current.status).toBe("success")
227
+ })
228
+ })
229
+ })
230
+
231
+ describe("refetching", () => {
232
+ it("should support manual refetch", async () => {
233
+ const { result } = renderHook(
234
+ () =>
235
+ useActorSuspenseQuery<TestActor, "greet">({
236
+ reactor: mockReactor,
237
+ functionName: "greet",
238
+ }),
239
+ { wrapper }
240
+ )
241
+
242
+ await waitFor(() => {
243
+ expect(result.current.isSuccess).toBe(true)
244
+ })
245
+
246
+ // Refetch should be available
247
+ expect(typeof result.current.refetch).toBe("function")
248
+
249
+ await result.current.refetch()
250
+
251
+ expect(result.current.isSuccess).toBe(true)
252
+ })
253
+ })
254
+ })
@@ -0,0 +1,112 @@
1
+ import { useMemo } from "react"
2
+ import {
3
+ QueryKey,
4
+ useSuspenseQuery,
5
+ QueryObserverOptions,
6
+ UseSuspenseQueryResult,
7
+ QueryFunction,
8
+ } from "@tanstack/react-query"
9
+ import {
10
+ FunctionName,
11
+ Reactor,
12
+ TransformKey,
13
+ ReactorArgs,
14
+ ReactorReturnOk,
15
+ ReactorReturnErr,
16
+ } from "@ic-reactor/core"
17
+ import { CallConfig } from "@icp-sdk/core/agent"
18
+
19
+ export interface UseActorSuspenseQueryParameters<
20
+ A,
21
+ M extends FunctionName<A>,
22
+ T extends TransformKey = "candid",
23
+ TSelected = ReactorReturnOk<A, M, T>,
24
+ > extends Omit<
25
+ QueryObserverOptions<
26
+ ReactorReturnOk<A, M, T>,
27
+ ReactorReturnErr<A, M, T>,
28
+ TSelected,
29
+ ReactorReturnOk<A, M, T>,
30
+ QueryKey
31
+ >,
32
+ "queryKey" | "queryFn"
33
+ > {
34
+ reactor: Reactor<A, T>
35
+ functionName: M
36
+ args?: ReactorArgs<A, M, T>
37
+ callConfig?: CallConfig
38
+ queryKey?: QueryKey
39
+ }
40
+
41
+ export type UseActorSuspenseQueryConfig<
42
+ A,
43
+ M extends FunctionName<A>,
44
+ T extends TransformKey = "candid",
45
+ TSelected = ReactorReturnOk<A, M, T>,
46
+ > = Omit<UseActorSuspenseQueryParameters<A, M, T, TSelected>, "reactor">
47
+
48
+ export type UseActorSuspenseQueryResult<
49
+ A,
50
+ M extends FunctionName<A>,
51
+ T extends TransformKey = "candid",
52
+ TSelected = ReactorReturnOk<A, M, T>,
53
+ > = UseSuspenseQueryResult<TSelected, ReactorReturnErr<A, M, T>>
54
+
55
+ /**
56
+ * Hook for executing suspense-enabled query calls on a canister.
57
+ *
58
+ * @example
59
+ * const { data } = useActorSuspenseQuery({
60
+ * reactor,
61
+ * functionName: "getUser",
62
+ * args: ["user-123"],
63
+ * })
64
+ *
65
+ * // With select transformation
66
+ * const { data } = useActorSuspenseQuery({
67
+ * reactor,
68
+ * functionName: "getUser",
69
+ * args: ["user-123"],
70
+ * select: (user) => user.name,
71
+ * })
72
+ */
73
+ export const useActorSuspenseQuery = <
74
+ A,
75
+ M extends FunctionName<A>,
76
+ T extends TransformKey = "candid",
77
+ TSelected = ReactorReturnOk<A, M, T>,
78
+ >({
79
+ reactor,
80
+ functionName,
81
+ args,
82
+ callConfig,
83
+ queryKey: defaultQueryKey,
84
+ ...options
85
+ }: UseActorSuspenseQueryParameters<
86
+ A,
87
+ M,
88
+ T,
89
+ TSelected
90
+ >): UseActorSuspenseQueryResult<A, M, T, TSelected> => {
91
+ // Memoize query options to prevent unnecessary re-computations
92
+ const { queryKey, queryFn } = useMemo(
93
+ () =>
94
+ reactor.getQueryOptions<M>({
95
+ callConfig,
96
+ functionName,
97
+ args,
98
+ queryKey: defaultQueryKey,
99
+ }),
100
+ [reactor, callConfig, functionName, args, defaultQueryKey]
101
+ )
102
+
103
+ return useSuspenseQuery(
104
+ {
105
+ // Suspense queries don't support skipToken, so cast the queryFn
106
+ queryFn: queryFn as QueryFunction<ReactorReturnOk<A, M, T>, QueryKey>,
107
+ ...options,
108
+ queryKey,
109
+ },
110
+ reactor.queryClient
111
+ )
112
+ }
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ // Re-export core (following TanStack Query's pattern)
2
+ // Users can import everything from @ic-reactor/react without needing @ic-reactor/core
3
+ export * from "@ic-reactor/core"
4
+
5
+ // Re-export hooks
6
+ export * from "./hooks"
7
+
8
+ // Validation utilities for React
9
+ export * from "./validation"
10
+
11
+ // React-specific exports
12
+ export * from "./createAuthHooks"
13
+ export * from "./createActorHooks"
14
+
15
+ export * from "./createQuery"
16
+ export * from "./createSuspenseQuery"
17
+ export * from "./createInfiniteQuery"
18
+ export * from "./createSuspenseInfiniteQuery"
19
+ export * from "./createMutation"
20
+
21
+ export * from "./types"