@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.
- package/README.md +11 -10
- package/dist/createActorHooks.d.ts +2 -0
- package/dist/createActorHooks.d.ts.map +1 -1
- package/dist/createActorHooks.js +2 -0
- package/dist/createActorHooks.js.map +1 -1
- package/dist/createMutation.d.ts.map +1 -1
- package/dist/createMutation.js +4 -0
- package/dist/createMutation.js.map +1 -1
- package/dist/hooks/index.d.ts +18 -5
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +15 -5
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.d.ts +13 -11
- package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorMethod.d.ts +105 -0
- package/dist/hooks/useActorMethod.d.ts.map +1 -0
- package/dist/hooks/useActorMethod.js +192 -0
- package/dist/hooks/useActorMethod.js.map +1 -0
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +13 -10
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
- package/package.json +9 -8
- package/src/createActorHooks.ts +146 -0
- package/src/createAuthHooks.ts +137 -0
- package/src/createInfiniteQuery.ts +471 -0
- package/src/createMutation.ts +163 -0
- package/src/createQuery.ts +197 -0
- package/src/createSuspenseInfiniteQuery.ts +478 -0
- package/src/createSuspenseQuery.ts +215 -0
- package/src/hooks/index.ts +93 -0
- package/src/hooks/useActorInfiniteQuery.test.tsx +457 -0
- package/src/hooks/useActorInfiniteQuery.ts +134 -0
- package/src/hooks/useActorMethod.test.tsx +798 -0
- package/src/hooks/useActorMethod.ts +397 -0
- package/src/hooks/useActorMutation.test.tsx +220 -0
- package/src/hooks/useActorMutation.ts +124 -0
- package/src/hooks/useActorQuery.test.tsx +287 -0
- package/src/hooks/useActorQuery.ts +110 -0
- package/src/hooks/useActorSuspenseInfiniteQuery.test.tsx +472 -0
- package/src/hooks/useActorSuspenseInfiniteQuery.ts +137 -0
- package/src/hooks/useActorSuspenseQuery.test.tsx +254 -0
- package/src/hooks/useActorSuspenseQuery.ts +112 -0
- package/src/index.ts +21 -0
- package/src/types.ts +435 -0
- 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"
|