@ic-reactor/react 3.3.0 → 3.4.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 +117 -4
- package/dist/createActorHooks.d.ts +1 -1
- package/dist/createActorHooks.d.ts.map +1 -1
- package/dist/createActorHooks.js +10 -25
- package/dist/createActorHooks.js.map +1 -1
- package/dist/createAuthHooks.d.ts +13 -5
- package/dist/createAuthHooks.d.ts.map +1 -1
- package/dist/createAuthHooks.js +51 -1
- package/dist/createAuthHooks.js.map +1 -1
- package/dist/createInfiniteQuery.d.ts.map +1 -1
- package/dist/createInfiniteQuery.js +2 -12
- package/dist/createInfiniteQuery.js.map +1 -1
- package/dist/createMutation.d.ts.map +1 -1
- package/dist/createMutation.js +39 -56
- package/dist/createMutation.js.map +1 -1
- package/dist/createQuery.d.ts.map +1 -1
- package/dist/createQuery.js +31 -39
- package/dist/createQuery.js.map +1 -1
- package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/createSuspenseInfiniteQuery.js +1 -14
- package/dist/createSuspenseInfiniteQuery.js.map +1 -1
- package/dist/createSuspenseQuery.d.ts.map +1 -1
- package/dist/createSuspenseQuery.js +30 -39
- package/dist/createSuspenseQuery.js.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.js +4 -5
- package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorMutation.d.ts +9 -2
- package/dist/hooks/useActorMutation.d.ts.map +1 -1
- package/dist/hooks/useActorMutation.js +15 -15
- package/dist/hooks/useActorMutation.js.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.js +4 -5
- package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +26 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +41 -0
- package/dist/utils.js.map +1 -0
- package/package.json +17 -11
- package/src/createActorHooks.ts +32 -31
- package/src/createAuthHooks.ts +93 -6
- package/src/createInfiniteQuery.ts +2 -19
- package/src/createMutation.ts +52 -69
- package/src/createQuery.ts +41 -52
- package/src/createSuspenseInfiniteQuery.ts +1 -22
- package/src/createSuspenseQuery.ts +42 -56
- package/src/hooks/useActorInfiniteQuery.ts +4 -9
- package/src/hooks/useActorMutation.ts +40 -14
- package/src/hooks/useActorSuspenseInfiniteQuery.ts +4 -9
- package/src/types.ts +32 -0
- package/src/utils.ts +52 -0
package/src/createQuery.ts
CHANGED
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
QueryFactoryConfig,
|
|
31
31
|
NoInfer,
|
|
32
32
|
} from "./types"
|
|
33
|
+
import { buildChainedSelect } from "./utils"
|
|
33
34
|
|
|
34
35
|
// ============================================================================
|
|
35
36
|
// Internal Implementation
|
|
@@ -56,37 +57,34 @@ const createQueryImpl = <
|
|
|
56
57
|
...rest
|
|
57
58
|
} = config
|
|
58
59
|
|
|
59
|
-
const params = {
|
|
60
|
-
functionName,
|
|
61
|
-
args,
|
|
62
|
-
queryKey: customQueryKey,
|
|
63
|
-
}
|
|
60
|
+
const params = { functionName, args, queryKey: customQueryKey }
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
const getQueryKey = (): QueryKey => {
|
|
67
|
-
return reactor.generateQueryKey(params)
|
|
68
|
-
}
|
|
62
|
+
const getQueryKey = (): QueryKey => reactor.generateQueryKey(params)
|
|
69
63
|
|
|
70
|
-
//
|
|
64
|
+
// Apply config.select to raw data (shared by fetch, getCacheData, and the hook)
|
|
65
|
+
const applySelect = (raw: TData): TSelected =>
|
|
66
|
+
select ? select(raw) : (raw as unknown as TSelected)
|
|
67
|
+
|
|
68
|
+
/** Cache-first fetch for use in loaders / route preloading. */
|
|
71
69
|
const fetch = async (): Promise<TSelected> => {
|
|
72
70
|
const result = await reactor.fetchQuery(params)
|
|
73
|
-
return
|
|
71
|
+
return applySelect(result)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Fire-and-forget prefetch — warms the cache without blocking. */
|
|
75
|
+
const prefetch = (): Promise<void> => {
|
|
76
|
+
const baseOptions = reactor.getQueryOptions(params)
|
|
77
|
+
return reactor.queryClient.prefetchQuery({
|
|
78
|
+
queryKey: baseOptions.queryKey,
|
|
79
|
+
queryFn: baseOptions.queryFn,
|
|
80
|
+
staleTime,
|
|
81
|
+
})
|
|
74
82
|
}
|
|
75
83
|
|
|
76
|
-
// Implementation
|
|
77
84
|
const useQueryHook: UseQueryWithSelect<TData, TSelected, TError> = (
|
|
78
85
|
options: any
|
|
79
86
|
): any => {
|
|
80
87
|
const baseOptions = reactor.getQueryOptions(params)
|
|
81
|
-
|
|
82
|
-
// Chain the selects: raw -> config.select -> options.select
|
|
83
|
-
const chainedSelect = (rawData: TData) => {
|
|
84
|
-
const firstPass = config.select ? config.select(rawData) : rawData
|
|
85
|
-
if (options?.select) {
|
|
86
|
-
return options.select(firstPass)
|
|
87
|
-
}
|
|
88
|
-
return firstPass
|
|
89
|
-
}
|
|
90
88
|
return useQuery(
|
|
91
89
|
{
|
|
92
90
|
queryKey: baseOptions.queryKey,
|
|
@@ -94,50 +92,46 @@ const createQueryImpl = <
|
|
|
94
92
|
...rest,
|
|
95
93
|
...options,
|
|
96
94
|
queryFn: baseOptions.queryFn,
|
|
97
|
-
select:
|
|
95
|
+
select: buildChainedSelect(select, options?.select),
|
|
98
96
|
},
|
|
99
97
|
reactor.queryClient
|
|
100
98
|
)
|
|
101
99
|
}
|
|
102
100
|
|
|
103
|
-
// Invalidate function
|
|
104
101
|
const invalidate = async (): Promise<void> => {
|
|
105
|
-
|
|
106
|
-
await reactor.queryClient.invalidateQueries({ queryKey })
|
|
102
|
+
await reactor.queryClient.invalidateQueries({ queryKey: getQueryKey() })
|
|
107
103
|
}
|
|
108
104
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// Apply config.select to raw cache data
|
|
118
|
-
const selectedData = (
|
|
119
|
-
config.select ? config.select(cachedRawData) : cachedRawData
|
|
120
|
-
) as TData
|
|
121
|
-
|
|
122
|
-
// Apply optional select parameter
|
|
123
|
-
if (selectFn) {
|
|
124
|
-
return selectFn(selectedData)
|
|
125
|
-
}
|
|
105
|
+
const getCacheData: QueryResult<TData, TSelected, TError>["getCacheData"] = (
|
|
106
|
+
selectFn?: (data: TSelected) => unknown
|
|
107
|
+
): any => {
|
|
108
|
+
const raw = reactor.getQueryData(params)
|
|
109
|
+
if (raw === undefined) return undefined
|
|
110
|
+
const selected = applySelect(raw)
|
|
111
|
+
return selectFn ? selectFn(selected) : selected
|
|
112
|
+
}
|
|
126
113
|
|
|
127
|
-
|
|
114
|
+
const setData: QueryResult<TData, TSelected, TError>["setData"] = (
|
|
115
|
+
updater
|
|
116
|
+
) => {
|
|
117
|
+
return reactor.queryClient.setQueryData(getQueryKey(), updater as any) as
|
|
118
|
+
| TData
|
|
119
|
+
| undefined
|
|
128
120
|
}
|
|
129
121
|
|
|
130
122
|
return {
|
|
131
123
|
fetch,
|
|
124
|
+
prefetch,
|
|
132
125
|
useQuery: useQueryHook,
|
|
133
126
|
invalidate,
|
|
134
127
|
getQueryKey,
|
|
135
128
|
getCacheData,
|
|
129
|
+
setData,
|
|
136
130
|
}
|
|
137
131
|
}
|
|
138
132
|
|
|
139
133
|
// ============================================================================
|
|
140
|
-
// Factory Function
|
|
134
|
+
// Public Factory Function
|
|
141
135
|
// ============================================================================
|
|
142
136
|
|
|
143
137
|
export function createQuery<
|
|
@@ -172,26 +166,21 @@ export function createQueryFactory<
|
|
|
172
166
|
QueryResult<QueryFnData<A, M, T>, TSelected, QueryError<A, M, T>>
|
|
173
167
|
>()
|
|
174
168
|
|
|
175
|
-
return (
|
|
176
|
-
args: ReactorArgs<A, M, T>
|
|
177
|
-
): QueryResult<QueryFnData<A, M, T>, TSelected, QueryError<A, M, T>> => {
|
|
169
|
+
return (args: ReactorArgs<A, M, T>) => {
|
|
178
170
|
const key = reactor.generateQueryKey({
|
|
179
171
|
functionName: config.functionName as M,
|
|
180
172
|
args,
|
|
181
173
|
})
|
|
182
174
|
const cacheKey = JSON.stringify(key)
|
|
183
175
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
176
|
+
const existing = cache.get(cacheKey)
|
|
177
|
+
if (existing) return existing
|
|
187
178
|
|
|
188
179
|
const result = createQueryImpl<A, M, T, TSelected>(reactor, {
|
|
189
180
|
...config,
|
|
190
181
|
args,
|
|
191
182
|
})
|
|
192
|
-
|
|
193
183
|
cache.set(cacheKey, result)
|
|
194
|
-
|
|
195
184
|
return result
|
|
196
185
|
}
|
|
197
186
|
}
|
|
@@ -45,8 +45,7 @@ import {
|
|
|
45
45
|
} from "@tanstack/react-query"
|
|
46
46
|
import { CallConfig } from "@icp-sdk/core/agent"
|
|
47
47
|
import { NoInfer } from "./types"
|
|
48
|
-
|
|
49
|
-
const FACTORY_KEY_ARGS_QUERY_KEY = "__ic_reactor_factory_key_args"
|
|
48
|
+
import { mergeFactoryQueryKey } from "./utils"
|
|
50
49
|
|
|
51
50
|
type SuspenseInfiniteFactoryCallOptions = {
|
|
52
51
|
queryKey?: QueryKey
|
|
@@ -78,26 +77,6 @@ type SuspenseInfiniteQueryFactoryFn<
|
|
|
78
77
|
>
|
|
79
78
|
}
|
|
80
79
|
|
|
81
|
-
const mergeFactoryQueryKey = (
|
|
82
|
-
baseQueryKey?: QueryKey,
|
|
83
|
-
callQueryKey?: QueryKey,
|
|
84
|
-
keyArgs?: unknown
|
|
85
|
-
): QueryKey | undefined => {
|
|
86
|
-
const merged: unknown[] = []
|
|
87
|
-
|
|
88
|
-
if (baseQueryKey) {
|
|
89
|
-
merged.push(...baseQueryKey)
|
|
90
|
-
}
|
|
91
|
-
if (callQueryKey) {
|
|
92
|
-
merged.push(...callQueryKey)
|
|
93
|
-
}
|
|
94
|
-
if (keyArgs !== undefined) {
|
|
95
|
-
merged.push({ [FACTORY_KEY_ARGS_QUERY_KEY]: keyArgs })
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return merged.length > 0 ? merged : undefined
|
|
99
|
-
}
|
|
100
|
-
|
|
101
80
|
// ============================================================================
|
|
102
81
|
// Type Definitions
|
|
103
82
|
// ============================================================================
|
|
@@ -35,6 +35,7 @@ import type {
|
|
|
35
35
|
SuspenseQueryFactoryConfig,
|
|
36
36
|
NoInfer,
|
|
37
37
|
} from "./types"
|
|
38
|
+
import { buildChainedSelect } from "./utils"
|
|
38
39
|
|
|
39
40
|
// ============================================================================
|
|
40
41
|
// Internal Implementation
|
|
@@ -65,39 +66,35 @@ const createSuspenseQueryImpl = <
|
|
|
65
66
|
...rest
|
|
66
67
|
} = config
|
|
67
68
|
|
|
68
|
-
const params = {
|
|
69
|
-
functionName,
|
|
70
|
-
args,
|
|
71
|
-
queryKey: customQueryKey,
|
|
72
|
-
}
|
|
69
|
+
const params = { functionName, args, queryKey: customQueryKey }
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
const getQueryKey = () => reactor.generateQueryKey(params)
|
|
72
|
+
|
|
73
|
+
const applySelect = (raw: TData): TSelected =>
|
|
74
|
+
select ? select(raw) : (raw as unknown as TSelected)
|
|
78
75
|
|
|
79
|
-
|
|
76
|
+
/** Cache-first fetch for use in loaders / route preloading. */
|
|
80
77
|
const fetch = async (): Promise<TSelected> => {
|
|
81
78
|
const result = await reactor.fetchQuery(params)
|
|
82
|
-
return
|
|
79
|
+
return applySelect(result)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Fire-and-forget prefetch — warms the cache without blocking. */
|
|
83
|
+
const prefetch = (): Promise<void> => {
|
|
84
|
+
const baseOptions = reactor.getQueryOptions(params)
|
|
85
|
+
return reactor.queryClient.prefetchQuery({
|
|
86
|
+
queryKey: baseOptions.queryKey,
|
|
87
|
+
queryFn: baseOptions.queryFn,
|
|
88
|
+
staleTime,
|
|
89
|
+
})
|
|
83
90
|
}
|
|
84
91
|
|
|
85
|
-
// Implementation
|
|
86
92
|
const useSuspenseQueryHook: UseSuspenseQueryWithSelect<
|
|
87
93
|
TData,
|
|
88
94
|
TSelected,
|
|
89
95
|
TError
|
|
90
96
|
> = (options: any): any => {
|
|
91
97
|
const baseOptions = reactor.getQueryOptions(params)
|
|
92
|
-
|
|
93
|
-
// Chain the selects: raw -> config.select -> options.select
|
|
94
|
-
const chainedSelect = (rawData: TData) => {
|
|
95
|
-
const firstPass = config.select ? config.select(rawData) : rawData
|
|
96
|
-
if (options?.select) {
|
|
97
|
-
return options.select(firstPass)
|
|
98
|
-
}
|
|
99
|
-
return firstPass
|
|
100
|
-
}
|
|
101
98
|
return useSuspenseQuery(
|
|
102
99
|
{
|
|
103
100
|
queryKey: baseOptions.queryKey,
|
|
@@ -105,50 +102,48 @@ const createSuspenseQueryImpl = <
|
|
|
105
102
|
...rest,
|
|
106
103
|
...options,
|
|
107
104
|
queryFn: baseOptions.queryFn,
|
|
108
|
-
select:
|
|
105
|
+
select: buildChainedSelect(select, options?.select),
|
|
109
106
|
},
|
|
110
107
|
reactor.queryClient
|
|
111
108
|
)
|
|
112
109
|
}
|
|
113
110
|
|
|
114
|
-
// Invalidate function
|
|
115
111
|
const invalidate = async (): Promise<void> => {
|
|
116
|
-
|
|
117
|
-
await reactor.queryClient.invalidateQueries({ queryKey })
|
|
112
|
+
await reactor.queryClient.invalidateQueries({ queryKey: getQueryKey() })
|
|
118
113
|
}
|
|
119
114
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
config.select ? config.select(cachedRawData) : cachedRawData
|
|
131
|
-
) as TData
|
|
132
|
-
|
|
133
|
-
// Apply optional select parameter
|
|
134
|
-
if (selectFn) {
|
|
135
|
-
return selectFn(selectedData)
|
|
136
|
-
}
|
|
115
|
+
const getCacheData: SuspenseQueryResult<
|
|
116
|
+
TData,
|
|
117
|
+
TSelected,
|
|
118
|
+
TError
|
|
119
|
+
>["getCacheData"] = (selectFn?: (data: TSelected) => unknown): any => {
|
|
120
|
+
const raw = reactor.getQueryData(params)
|
|
121
|
+
if (raw === undefined) return undefined
|
|
122
|
+
const selected = applySelect(raw)
|
|
123
|
+
return selectFn ? selectFn(selected) : selected
|
|
124
|
+
}
|
|
137
125
|
|
|
138
|
-
|
|
126
|
+
const setData: SuspenseQueryResult<TData, TSelected, TError>["setData"] = (
|
|
127
|
+
updater
|
|
128
|
+
) => {
|
|
129
|
+
return reactor.queryClient.setQueryData(getQueryKey(), updater as any) as
|
|
130
|
+
| TData
|
|
131
|
+
| undefined
|
|
139
132
|
}
|
|
140
133
|
|
|
141
134
|
return {
|
|
142
135
|
fetch,
|
|
136
|
+
prefetch,
|
|
143
137
|
useSuspenseQuery: useSuspenseQueryHook,
|
|
144
138
|
invalidate,
|
|
145
139
|
getQueryKey,
|
|
146
140
|
getCacheData,
|
|
141
|
+
setData,
|
|
147
142
|
}
|
|
148
143
|
}
|
|
149
144
|
|
|
150
145
|
// ============================================================================
|
|
151
|
-
// Factory Function
|
|
146
|
+
// Public Factory Function
|
|
152
147
|
// ============================================================================
|
|
153
148
|
|
|
154
149
|
export function createSuspenseQuery<
|
|
@@ -186,30 +181,21 @@ export function createSuspenseQueryFactory<
|
|
|
186
181
|
SuspenseQueryResult<QueryFnData<A, M, T>, TSelected, QueryError<A, M, T>>
|
|
187
182
|
>()
|
|
188
183
|
|
|
189
|
-
return (
|
|
190
|
-
args: ReactorArgs<A, M, T>
|
|
191
|
-
): SuspenseQueryResult<
|
|
192
|
-
QueryFnData<A, M, T>,
|
|
193
|
-
TSelected,
|
|
194
|
-
QueryError<A, M, T>
|
|
195
|
-
> => {
|
|
184
|
+
return (args: ReactorArgs<A, M, T>) => {
|
|
196
185
|
const key = reactor.generateQueryKey({
|
|
197
186
|
functionName: config.functionName as M,
|
|
198
187
|
args,
|
|
199
188
|
})
|
|
200
189
|
const cacheKey = JSON.stringify(key)
|
|
201
190
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
191
|
+
const existing = cache.get(cacheKey)
|
|
192
|
+
if (existing) return existing
|
|
205
193
|
|
|
206
194
|
const result = createSuspenseQueryImpl<A, M, T, TSelected>(reactor, {
|
|
207
195
|
...(config as SuspenseQueryFactoryConfig<A, M, T, TSelected>),
|
|
208
196
|
args,
|
|
209
197
|
})
|
|
210
|
-
|
|
211
198
|
cache.set(cacheKey, result)
|
|
212
|
-
|
|
213
199
|
return result
|
|
214
200
|
}
|
|
215
201
|
}
|
|
@@ -104,16 +104,11 @@ export const useActorInfiniteQuery = <
|
|
|
104
104
|
T,
|
|
105
105
|
TPageParam
|
|
106
106
|
>): UseActorInfiniteQueryResult<A, M, T, TPageParam> => {
|
|
107
|
-
//
|
|
107
|
+
// Always pass queryKey through generateQueryKey so it is merged with the
|
|
108
|
+
// reactor/function identity. Using the custom key verbatim would cause cache
|
|
109
|
+
// collisions if two different actors or methods share the same key string.
|
|
108
110
|
const baseQueryKey = useMemo(
|
|
109
|
-
() =>
|
|
110
|
-
queryKey ??
|
|
111
|
-
reactor.generateQueryKey(
|
|
112
|
-
{
|
|
113
|
-
functionName,
|
|
114
|
-
},
|
|
115
|
-
callConfig
|
|
116
|
-
),
|
|
111
|
+
() => reactor.generateQueryKey({ functionName, queryKey }, callConfig),
|
|
117
112
|
[queryKey, reactor, functionName, callConfig]
|
|
118
113
|
)
|
|
119
114
|
|
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
FunctionName,
|
|
13
13
|
TransformKey,
|
|
14
14
|
ReactorReturnErr,
|
|
15
|
+
isCanisterError,
|
|
16
|
+
CanisterError,
|
|
17
|
+
ErrResult,
|
|
18
|
+
ActorMethodReturnType,
|
|
19
|
+
TransformReturnRegistry,
|
|
15
20
|
} from "@ic-reactor/core"
|
|
16
21
|
import { CallConfig } from "@icp-sdk/core/agent"
|
|
17
22
|
|
|
@@ -31,6 +36,17 @@ export interface UseActorMutationParameters<
|
|
|
31
36
|
functionName: M
|
|
32
37
|
callConfig?: CallConfig
|
|
33
38
|
invalidateQueries?: QueryKey[]
|
|
39
|
+
/**
|
|
40
|
+
* Callback for canister-level business logic errors.
|
|
41
|
+
* Called when the canister returns a Result { Err: E } variant.
|
|
42
|
+
* Separate from `onError`, which fires for all errors including network failures.
|
|
43
|
+
*/
|
|
44
|
+
onCanisterError?: (
|
|
45
|
+
error: CanisterError<
|
|
46
|
+
TransformReturnRegistry<ErrResult<ActorMethodReturnType<A[M]>>>[T]
|
|
47
|
+
>,
|
|
48
|
+
variables: ReactorArgs<A, M, T>
|
|
49
|
+
) => void
|
|
34
50
|
}
|
|
35
51
|
|
|
36
52
|
export type UseActorMutationConfig<
|
|
@@ -57,6 +73,7 @@ export type UseActorMutationResult<
|
|
|
57
73
|
* reactor,
|
|
58
74
|
* functionName: "transfer",
|
|
59
75
|
* onSuccess: () => console.log("Success!"),
|
|
76
|
+
* onCanisterError: (err) => console.error("Canister Err:", err.code),
|
|
60
77
|
* })
|
|
61
78
|
*/
|
|
62
79
|
export const useActorMutation = <
|
|
@@ -68,22 +85,17 @@ export const useActorMutation = <
|
|
|
68
85
|
functionName,
|
|
69
86
|
invalidateQueries,
|
|
70
87
|
onSuccess,
|
|
88
|
+
onError,
|
|
89
|
+
onCanisterError,
|
|
71
90
|
callConfig,
|
|
72
91
|
...options
|
|
73
92
|
}: UseActorMutationParameters<A, M, T>): UseActorMutationResult<A, M, T> => {
|
|
74
|
-
// Memoize mutationFn to avoid creating new function on every render
|
|
75
93
|
const mutationFn = useCallback(
|
|
76
|
-
async (args: ReactorArgs<A, M, T>) =>
|
|
77
|
-
|
|
78
|
-
functionName,
|
|
79
|
-
callConfig,
|
|
80
|
-
args,
|
|
81
|
-
})
|
|
82
|
-
},
|
|
94
|
+
async (args: ReactorArgs<A, M, T>) =>
|
|
95
|
+
reactor.callMethod({ functionName, callConfig, args }),
|
|
83
96
|
[reactor, functionName, callConfig]
|
|
84
97
|
)
|
|
85
98
|
|
|
86
|
-
// Memoize onSuccess handler
|
|
87
99
|
const handleSuccess = useCallback(
|
|
88
100
|
async (
|
|
89
101
|
...params: Parameters<
|
|
@@ -103,21 +115,35 @@ export const useActorMutation = <
|
|
|
103
115
|
)
|
|
104
116
|
)
|
|
105
117
|
}
|
|
106
|
-
|
|
107
|
-
await onSuccess(...params)
|
|
108
|
-
}
|
|
118
|
+
await onSuccess?.(...params)
|
|
109
119
|
},
|
|
110
120
|
[reactor, invalidateQueries, onSuccess]
|
|
111
121
|
)
|
|
112
122
|
|
|
113
|
-
|
|
123
|
+
const handleError = useCallback(
|
|
124
|
+
(
|
|
125
|
+
error: ReactorReturnErr<A, M, T>,
|
|
126
|
+
variables: ReactorArgs<A, M, T>,
|
|
127
|
+
context: unknown,
|
|
128
|
+
mutation: unknown
|
|
129
|
+
) => {
|
|
130
|
+
if (isCanisterError(error)) {
|
|
131
|
+
onCanisterError?.(error as any, variables)
|
|
132
|
+
}
|
|
133
|
+
onError?.(error, variables, context as any, mutation as any)
|
|
134
|
+
},
|
|
135
|
+
[onCanisterError, onError]
|
|
136
|
+
)
|
|
137
|
+
|
|
114
138
|
const mutationOptions = useMemo(
|
|
115
139
|
() => ({
|
|
116
140
|
...options,
|
|
117
141
|
mutationFn,
|
|
118
142
|
onSuccess: handleSuccess,
|
|
143
|
+
onError: handleError,
|
|
119
144
|
}),
|
|
120
|
-
|
|
145
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
146
|
+
[mutationFn, handleSuccess, handleError]
|
|
121
147
|
)
|
|
122
148
|
|
|
123
149
|
return useMutation(mutationOptions, reactor.queryClient)
|
|
@@ -107,16 +107,11 @@ export const useActorSuspenseInfiniteQuery = <
|
|
|
107
107
|
T,
|
|
108
108
|
TPageParam
|
|
109
109
|
>): UseActorSuspenseInfiniteQueryResult<A, M, T, TPageParam> => {
|
|
110
|
-
//
|
|
110
|
+
// Always pass queryKey through generateQueryKey so it is merged with the
|
|
111
|
+
// reactor/function identity. Using the custom key verbatim would cause cache
|
|
112
|
+
// collisions if two different actors or methods share the same key string.
|
|
111
113
|
const baseQueryKey = useMemo(
|
|
112
|
-
() =>
|
|
113
|
-
queryKey ??
|
|
114
|
-
reactor.generateQueryKey(
|
|
115
|
-
{
|
|
116
|
-
functionName,
|
|
117
|
-
},
|
|
118
|
-
callConfig
|
|
119
|
-
),
|
|
114
|
+
() => reactor.generateQueryKey({ functionName, queryKey }, callConfig),
|
|
120
115
|
[queryKey, reactor, functionName, callConfig]
|
|
121
116
|
)
|
|
122
117
|
|
package/src/types.ts
CHANGED
|
@@ -231,6 +231,18 @@ export interface BaseQueryResult<
|
|
|
231
231
|
/** Fetch data in loader (uses ensureQueryData for cache-first) */
|
|
232
232
|
fetch: () => Promise<TSelected>
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Eagerly prefetch data into the cache without blocking.
|
|
236
|
+
* Useful for preloading data before navigating to a route.
|
|
237
|
+
*
|
|
238
|
+
* Unlike `fetch()`, this returns a void promise so it can be fire-and-forget.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* // In a route hover handler
|
|
242
|
+
* button.addEventListener("mouseenter", () => userQuery.prefetch())
|
|
243
|
+
*/
|
|
244
|
+
prefetch: () => Promise<void>
|
|
245
|
+
|
|
234
246
|
/** Invalidate the cache (refetches if query is active) */
|
|
235
247
|
invalidate: () => Promise<void>
|
|
236
248
|
|
|
@@ -256,6 +268,26 @@ export interface BaseQueryResult<
|
|
|
256
268
|
(): TSelected | undefined
|
|
257
269
|
<TFinal>(select: (data: TSelected) => TFinal): TFinal | undefined
|
|
258
270
|
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Write raw data directly into the cache (useful for optimistic updates).
|
|
274
|
+
* Accepts a new value or an updater function that receives the current cached raw data.
|
|
275
|
+
*
|
|
276
|
+
* Note: The value is stored as raw (pre-select) data. Any active `select`
|
|
277
|
+
* transformations are automatically re-applied by React Query on the next render.
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* // Optimistic update before a mutation
|
|
281
|
+
* userQuery.setData({ id: "1", name: "Alice" })
|
|
282
|
+
*
|
|
283
|
+
* // Functional update
|
|
284
|
+
* counterQuery.setData((prev) => (prev ?? 0) + 1)
|
|
285
|
+
*/
|
|
286
|
+
setData: (
|
|
287
|
+
updater:
|
|
288
|
+
| TQueryFnData
|
|
289
|
+
| ((old: TQueryFnData | undefined) => TQueryFnData | undefined)
|
|
290
|
+
) => TQueryFnData | undefined
|
|
259
291
|
}
|
|
260
292
|
|
|
261
293
|
/**
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared internal utilities for query and mutation factories.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { QueryKey } from "@tanstack/react-query"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Internal query-key segment used to distinguish per-call factory args
|
|
9
|
+
* from the base query key. Not part of the public API.
|
|
10
|
+
*/
|
|
11
|
+
export const FACTORY_KEY_ARGS_QUERY_KEY = "__ic_reactor_factory_key_args"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Merge a base query key, optional per-call query key, and optional key-args
|
|
15
|
+
* into a single query key array.
|
|
16
|
+
*
|
|
17
|
+
* Used by createInfiniteQueryFactory and createSuspenseInfiniteQueryFactory to
|
|
18
|
+
* ensure each unique set of factory args produces a distinct cache entry.
|
|
19
|
+
*/
|
|
20
|
+
export function mergeFactoryQueryKey(
|
|
21
|
+
baseQueryKey?: QueryKey,
|
|
22
|
+
callQueryKey?: QueryKey,
|
|
23
|
+
keyArgs?: unknown
|
|
24
|
+
): QueryKey | undefined {
|
|
25
|
+
const merged: unknown[] = []
|
|
26
|
+
|
|
27
|
+
if (baseQueryKey) merged.push(...baseQueryKey)
|
|
28
|
+
if (callQueryKey) merged.push(...callQueryKey)
|
|
29
|
+
if (keyArgs !== undefined)
|
|
30
|
+
merged.push({ [FACTORY_KEY_ARGS_QUERY_KEY]: keyArgs })
|
|
31
|
+
|
|
32
|
+
return merged.length > 0 ? merged : undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build a chained select function that first applies the config-level select
|
|
37
|
+
* (if any) and then the hook-level select (if any).
|
|
38
|
+
*
|
|
39
|
+
* This enables `createQuery` / `createSuspenseQuery` to support two-level
|
|
40
|
+
* select chaining without duplicating the logic.
|
|
41
|
+
*/
|
|
42
|
+
export function buildChainedSelect<TData, TSelected, TFinal = TSelected>(
|
|
43
|
+
configSelect: ((data: TData) => TSelected) | undefined,
|
|
44
|
+
hookSelect: ((data: TSelected) => TFinal) | undefined
|
|
45
|
+
): (rawData: TData) => TSelected | TFinal {
|
|
46
|
+
return (rawData: TData) => {
|
|
47
|
+
const firstPass = configSelect
|
|
48
|
+
? configSelect(rawData)
|
|
49
|
+
: (rawData as unknown as TSelected)
|
|
50
|
+
return hookSelect ? hookSelect(firstPass) : firstPass
|
|
51
|
+
}
|
|
52
|
+
}
|