@ic-reactor/react 3.8.0 → 3.10.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 +84 -10
- package/dist/auth/authentication-manager.d.ts.map +1 -1
- package/dist/auth/authentication-manager.js +146 -21
- package/dist/auth/authentication-manager.js.map +1 -1
- package/dist/auth/createIdentityAttributeHooks.d.ts.map +1 -1
- package/dist/auth/createIdentityAttributeHooks.js +47 -5
- package/dist/auth/createIdentityAttributeHooks.js.map +1 -1
- package/dist/createInfiniteQuery.d.ts +11 -9
- package/dist/createInfiniteQuery.d.ts.map +1 -1
- package/dist/createInfiniteQuery.js +20 -17
- package/dist/createInfiniteQuery.js.map +1 -1
- package/dist/createMutation.d.ts.map +1 -1
- package/dist/createMutation.js +37 -3
- package/dist/createMutation.js.map +1 -1
- package/dist/createQuery.d.ts.map +1 -1
- package/dist/createQuery.js +7 -3
- package/dist/createQuery.js.map +1 -1
- package/dist/createSuspenseInfiniteQuery.d.ts +10 -9
- package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/createSuspenseInfiniteQuery.js +18 -16
- package/dist/createSuspenseInfiniteQuery.js.map +1 -1
- package/dist/createSuspenseQuery.d.ts.map +1 -1
- package/dist/createSuspenseQuery.js +7 -3
- package/dist/createSuspenseQuery.js.map +1 -1
- package/dist/defineReactor.d.ts.map +1 -1
- package/dist/defineReactor.js +17 -2
- package/dist/defineReactor.js.map +1 -1
- package/dist/hooks/createAuthHooks.d.ts.map +1 -1
- package/dist/hooks/createAuthHooks.js +13 -0
- package/dist/hooks/createAuthHooks.js.map +1 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.js +21 -2
- package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorMethod.d.ts.map +1 -1
- package/dist/hooks/useActorMethod.js +76 -33
- package/dist/hooks/useActorMethod.js.map +1 -1
- package/dist/hooks/useActorMutation.d.ts +8 -1
- package/dist/hooks/useActorMutation.d.ts.map +1 -1
- package/dist/hooks/useActorMutation.js +23 -7
- package/dist/hooks/useActorMutation.js.map +1 -1
- package/dist/hooks/useActorQuery.d.ts.map +1 -1
- package/dist/hooks/useActorQuery.js +12 -1
- package/dist/hooks/useActorQuery.js.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.js +21 -2
- package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorSuspenseQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseQuery.js +12 -1
- package/dist/hooks/useActorSuspenseQuery.js.map +1 -1
- package/dist/types.d.ts +8 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +27 -5
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +65 -9
- package/dist/utils.js.map +1 -1
- package/package.json +8 -2
- package/src/auth/authentication-manager.ts +162 -21
- package/src/auth/createIdentityAttributeHooks.ts +54 -5
- package/src/createInfiniteQuery.ts +48 -34
- package/src/createMutation.ts +48 -2
- package/src/createQuery.ts +17 -10
- package/src/createSuspenseInfiniteQuery.ts +44 -32
- package/src/createSuspenseQuery.ts +17 -10
- package/src/defineReactor.ts +24 -2
- package/src/hooks/createAuthHooks.ts +18 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useActorInfiniteQuery.ts +30 -3
- package/src/hooks/useActorMethod.ts +91 -34
- package/src/hooks/useActorMutation.ts +35 -12
- package/src/hooks/useActorQuery.ts +11 -1
- package/src/hooks/useActorSuspenseInfiniteQuery.ts +30 -3
- package/src/hooks/useActorSuspenseQuery.ts +11 -1
- package/src/types.ts +8 -2
- package/src/utils.ts +66 -10
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useCallback } from "react"
|
|
2
2
|
import {
|
|
3
3
|
useMutation,
|
|
4
4
|
UseMutationOptions,
|
|
@@ -35,7 +35,14 @@ export interface UseActorMutationParameters<
|
|
|
35
35
|
reactor: Reactor<Service, Transform>
|
|
36
36
|
functionName: Method
|
|
37
37
|
callConfig?: CallConfig
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Queries to invalidate upon successful mutation.
|
|
40
|
+
*
|
|
41
|
+
* `undefined` entries are skipped, so the common
|
|
42
|
+
* `[maybeQuery?.getQueryKey()]` idiom is safe when the optional query object
|
|
43
|
+
* is absent.
|
|
44
|
+
*/
|
|
45
|
+
invalidateQueries?: (QueryKey | undefined)[]
|
|
39
46
|
/**
|
|
40
47
|
* Callback for canister-level business logic errors.
|
|
41
48
|
* Called when the canister returns a Result { Err: E } variant.
|
|
@@ -126,10 +133,18 @@ export const useActorMutation = <
|
|
|
126
133
|
>
|
|
127
134
|
) => {
|
|
128
135
|
if (invalidateQueries) {
|
|
136
|
+
// Skip undefined entries. React Query reads `{ queryKey: undefined }`
|
|
137
|
+
// as "match everything", so a single undefined — which the natural
|
|
138
|
+
// `invalidateQueries: [maybeQuery?.getQueryKey()]` idiom produces
|
|
139
|
+
// whenever the optional query object is absent — would invalidate every
|
|
140
|
+
// query in the client, including an app's unrelated non-canister ones.
|
|
141
|
+
// `createMutation.invalidateAll` already filters the same way.
|
|
129
142
|
await Promise.all(
|
|
130
|
-
invalidateQueries
|
|
131
|
-
|
|
132
|
-
|
|
143
|
+
invalidateQueries
|
|
144
|
+
.filter((queryKey) => queryKey !== undefined)
|
|
145
|
+
.map((queryKey) =>
|
|
146
|
+
reactor.queryClient.invalidateQueries({ queryKey })
|
|
147
|
+
)
|
|
133
148
|
)
|
|
134
149
|
}
|
|
135
150
|
await onSuccess?.(...params)
|
|
@@ -152,16 +167,24 @@ export const useActorMutation = <
|
|
|
152
167
|
[onCanisterError, onError]
|
|
153
168
|
)
|
|
154
169
|
|
|
155
|
-
|
|
156
|
-
|
|
170
|
+
// Not memoized. The deps could only ever list the values destructured above,
|
|
171
|
+
// never the `options` rest bucket, so everything passed straight through —
|
|
172
|
+
// `onMutate`, `onSettled`, `retry`, `meta`, `gcTime` — was frozen at the first
|
|
173
|
+
// render: `useMutation` calls `observer.setOptions` from an effect keyed on
|
|
174
|
+
// the options identity, so a later render's closures never reached it. A
|
|
175
|
+
// component passing only `onSettled` would report the recipient selected at
|
|
176
|
+
// mount rather than at submit.
|
|
177
|
+
//
|
|
178
|
+
// `createMutation`'s own hook builds its options inline for the same reason.
|
|
179
|
+
// A fresh object per render costs nothing here: `setOptions` diffs the values
|
|
180
|
+
// rather than the reference.
|
|
181
|
+
return useMutation(
|
|
182
|
+
{
|
|
157
183
|
...options,
|
|
158
184
|
mutationFn,
|
|
159
185
|
onSuccess: handleSuccess,
|
|
160
186
|
onError: handleError,
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
[mutationFn, handleSuccess, handleError]
|
|
187
|
+
},
|
|
188
|
+
reactor.queryClient
|
|
164
189
|
)
|
|
165
|
-
|
|
166
|
-
return useMutation(mutationOptions, reactor.queryClient)
|
|
167
190
|
}
|
|
@@ -100,7 +100,17 @@ export const useActorQuery = <
|
|
|
100
100
|
args,
|
|
101
101
|
queryKey: defaultQueryKey,
|
|
102
102
|
}),
|
|
103
|
-
|
|
103
|
+
// `canisterId` is mutable reactor state that `setCanisterId` can change,
|
|
104
|
+
// while `reactor` itself stays the same object — so it has to be a
|
|
105
|
+
// dependency in its own right or the key stays pinned to the old canister.
|
|
106
|
+
[
|
|
107
|
+
reactor,
|
|
108
|
+
reactor.canisterId?.toString(),
|
|
109
|
+
callConfig,
|
|
110
|
+
functionName,
|
|
111
|
+
args,
|
|
112
|
+
defaultQueryKey,
|
|
113
|
+
]
|
|
104
114
|
)
|
|
105
115
|
|
|
106
116
|
return useQuery(
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
ReactorReturnErr,
|
|
17
17
|
} from "@ic-reactor/core"
|
|
18
18
|
import { CallConfig } from "@icp-sdk/core/agent"
|
|
19
|
-
import { normalizeQueryData } from "../utils.js"
|
|
19
|
+
import { mergeFactoryQueryKey, normalizeQueryData } from "../utils.js"
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Parameters for useActorSuspenseInfiniteQuery hook.
|
|
@@ -129,8 +129,35 @@ export const useActorSuspenseInfiniteQuery = <
|
|
|
129
129
|
// reactor/function identity. Using the custom key verbatim would cause cache
|
|
130
130
|
// collisions if two different actors or methods share the same key string.
|
|
131
131
|
const baseQueryKey = useMemo(
|
|
132
|
-
() =>
|
|
133
|
-
|
|
132
|
+
() =>
|
|
133
|
+
reactor.generateQueryKey(
|
|
134
|
+
{
|
|
135
|
+
functionName,
|
|
136
|
+
// Fold the call arguments into the key. They live in the `getArgs`
|
|
137
|
+
// closure rather than in the config, so without this two hooks on the
|
|
138
|
+
// same method with different arguments share one cache entry and
|
|
139
|
+
// serve each other's pages.
|
|
140
|
+
queryKey: mergeFactoryQueryKey(
|
|
141
|
+
queryKey,
|
|
142
|
+
undefined,
|
|
143
|
+
getArgs(options.initialPageParam)
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
callConfig
|
|
147
|
+
),
|
|
148
|
+
[
|
|
149
|
+
queryKey,
|
|
150
|
+
reactor,
|
|
151
|
+
// `canisterId` is mutable reactor state that `setCanisterId` can change,
|
|
152
|
+
// while `reactor` itself stays the same object — so it has to be a
|
|
153
|
+
// dependency in its own right or the key stays pinned to the old canister
|
|
154
|
+
// while the queryFn already calls the new one.
|
|
155
|
+
reactor.canisterId?.toString(),
|
|
156
|
+
functionName,
|
|
157
|
+
callConfig,
|
|
158
|
+
getArgs,
|
|
159
|
+
options.initialPageParam,
|
|
160
|
+
]
|
|
134
161
|
)
|
|
135
162
|
|
|
136
163
|
// Memoize queryFn to prevent recreation on every render
|
|
@@ -104,7 +104,17 @@ export const useActorSuspenseQuery = <
|
|
|
104
104
|
args,
|
|
105
105
|
queryKey: defaultQueryKey,
|
|
106
106
|
}),
|
|
107
|
-
|
|
107
|
+
// `canisterId` is mutable reactor state that `setCanisterId` can change,
|
|
108
|
+
// while `reactor` itself stays the same object — so it has to be a
|
|
109
|
+
// dependency in its own right or the key stays pinned to the old canister.
|
|
110
|
+
[
|
|
111
|
+
reactor,
|
|
112
|
+
reactor.canisterId?.toString(),
|
|
113
|
+
callConfig,
|
|
114
|
+
functionName,
|
|
115
|
+
args,
|
|
116
|
+
defaultQueryKey,
|
|
117
|
+
]
|
|
108
118
|
)
|
|
109
119
|
|
|
110
120
|
return useSuspenseQuery(
|
package/src/types.ts
CHANGED
|
@@ -356,8 +356,14 @@ export interface MutationConfig<
|
|
|
356
356
|
functionName: Method
|
|
357
357
|
/** Call configuration for the actor method */
|
|
358
358
|
callConfig?: CallConfig
|
|
359
|
-
/**
|
|
360
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Queries to invalidate upon successful mutation.
|
|
361
|
+
*
|
|
362
|
+
* `undefined` entries are skipped, so the common
|
|
363
|
+
* `[maybeQuery?.getQueryKey()]` idiom is safe when the optional query object
|
|
364
|
+
* is absent.
|
|
365
|
+
*/
|
|
366
|
+
invalidateQueries?: (QueryKey | undefined)[]
|
|
361
367
|
/**
|
|
362
368
|
* Callback for canister-level business logic errors.
|
|
363
369
|
* Called when the canister returns a Result { Err: E } variant.
|
package/src/utils.ts
CHANGED
|
@@ -46,20 +46,76 @@ export function mergeFactoryQueryKey(
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* Build a chained select
|
|
50
|
-
*
|
|
49
|
+
* Build a chained select that applies the config-level select (if any) and then
|
|
50
|
+
* the hook-level select (if any).
|
|
51
51
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* Returns the caller's own function untouched when only one of the two is
|
|
53
|
+
* present, and `undefined` when neither is — allocating a wrapper only for the
|
|
54
|
+
* genuinely chained case. Identity matters here: `QueryObserver` memoizes a
|
|
55
|
+
* select result on `options.select === previousSelectFn`, so a wrapper rebuilt
|
|
56
|
+
* on every render defeats that check. The select then re-runs each render, and
|
|
57
|
+
* for a select returning a non-plain value (a `Map`, a `Date`, a `Principal`)
|
|
58
|
+
* `replaceEqualDeep` cannot structurally share the result either, so `data`
|
|
59
|
+
* gets a fresh reference every render and a `useEffect([data])` that sets state
|
|
60
|
+
* becomes an unbounded loop.
|
|
61
|
+
*
|
|
62
|
+
* Callers should still memoize the result, since the chained case allocates.
|
|
54
63
|
*/
|
|
55
64
|
export function buildChainedSelect<TData, TSelected, TFinal = TSelected>(
|
|
56
65
|
configSelect: ((data: TData) => TSelected) | undefined,
|
|
57
66
|
hookSelect: ((data: TSelected) => TFinal) | undefined
|
|
58
|
-
): (rawData: TData) => TSelected | TFinal {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
): ((rawData: TData) => TSelected | TFinal) | undefined {
|
|
68
|
+
if (!configSelect) {
|
|
69
|
+
// `hookSelect` receives the raw data when there is no config-level select,
|
|
70
|
+
// matching the previous behaviour.
|
|
71
|
+
return hookSelect as unknown as
|
|
72
|
+
((rawData: TData) => TSelected | TFinal) | undefined
|
|
73
|
+
}
|
|
74
|
+
if (!hookSelect) return configSelect
|
|
75
|
+
return (rawData: TData) => hookSelect(configSelect(rawData))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* How many memoized query objects an args-late factory keeps.
|
|
80
|
+
*
|
|
81
|
+
* The memo exists so `getBalance(sameArgs)` returns the same object twice; it
|
|
82
|
+
* was unbounded, so an open-ended argument space — a per-principal balance in a
|
|
83
|
+
* long-lived SPA, a search-as-you-type filter, a cursor-keyed list — grew it
|
|
84
|
+
* forever (measured at ~2.9 kB per entry, 55.8 MB for 20k). A few hundred
|
|
85
|
+
* covers any realistic working set while capping the worst case.
|
|
86
|
+
*/
|
|
87
|
+
const FACTORY_CACHE_LIMIT = 256
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Smallest useful LRU: a `Map` already iterates in insertion order, so
|
|
91
|
+
* re-inserting on read is enough to track recency.
|
|
92
|
+
*
|
|
93
|
+
* Eviction is safe — it costs memoization, never correctness. A query object is
|
|
94
|
+
* a closure over the reactor and config, so one rebuilt after eviction behaves
|
|
95
|
+
* identically; only its reference identity differs.
|
|
96
|
+
*/
|
|
97
|
+
export function createBoundedCache<V>(limit: number = FACTORY_CACHE_LIMIT) {
|
|
98
|
+
const entries = new Map<string, V>()
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
get(key: string): V | undefined {
|
|
102
|
+
const value = entries.get(key)
|
|
103
|
+
if (value === undefined) return undefined
|
|
104
|
+
// Touch: move to the most-recent end.
|
|
105
|
+
entries.delete(key)
|
|
106
|
+
entries.set(key, value)
|
|
107
|
+
return value
|
|
108
|
+
},
|
|
109
|
+
set(key: string, value: V): void {
|
|
110
|
+
if (entries.has(key)) entries.delete(key)
|
|
111
|
+
else if (entries.size >= limit) {
|
|
112
|
+
const oldest = entries.keys().next().value
|
|
113
|
+
if (oldest !== undefined) entries.delete(oldest)
|
|
114
|
+
}
|
|
115
|
+
entries.set(key, value)
|
|
116
|
+
},
|
|
117
|
+
get size(): number {
|
|
118
|
+
return entries.size
|
|
119
|
+
},
|
|
64
120
|
}
|
|
65
121
|
}
|