@ic-reactor/react 3.12.5 → 3.13.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 (152) hide show
  1. package/README.md +410 -44
  2. package/dist/auth/auth-client-compat.d.ts +122 -0
  3. package/dist/auth/auth-client-compat.d.ts.map +1 -0
  4. package/dist/auth/auth-client-compat.js +162 -0
  5. package/dist/auth/auth-client-compat.js.map +1 -0
  6. package/dist/auth/authentication-manager.d.ts +287 -5
  7. package/dist/auth/authentication-manager.d.ts.map +1 -1
  8. package/dist/auth/authentication-manager.js +920 -150
  9. package/dist/auth/authentication-manager.js.map +1 -1
  10. package/dist/auth/createIdentityAttributeHooks.d.ts.map +1 -1
  11. package/dist/auth/createIdentityAttributeHooks.js +36 -20
  12. package/dist/auth/createIdentityAttributeHooks.js.map +1 -1
  13. package/dist/auth/identity-attributes-manager.d.ts +2 -1
  14. package/dist/auth/identity-attributes-manager.d.ts.map +1 -1
  15. package/dist/auth/identity-attributes-manager.js +90 -6
  16. package/dist/auth/identity-attributes-manager.js.map +1 -1
  17. package/dist/auth/identity-attributes.d.ts.map +1 -1
  18. package/dist/auth/identity-attributes.js +57 -0
  19. package/dist/auth/identity-attributes.js.map +1 -1
  20. package/dist/auth/local-ii-probe.d.ts +12 -1
  21. package/dist/auth/local-ii-probe.d.ts.map +1 -1
  22. package/dist/auth/local-ii-probe.js +22 -3
  23. package/dist/auth/local-ii-probe.js.map +1 -1
  24. package/dist/auth/types.d.ts +48 -5
  25. package/dist/auth/types.d.ts.map +1 -1
  26. package/dist/createActorHooks.d.ts +7 -18
  27. package/dist/createActorHooks.d.ts.map +1 -1
  28. package/dist/createActorHooks.js.map +1 -1
  29. package/dist/createInfiniteQuery.d.ts +51 -10
  30. package/dist/createInfiniteQuery.d.ts.map +1 -1
  31. package/dist/createInfiniteQuery.js +39 -15
  32. package/dist/createInfiniteQuery.js.map +1 -1
  33. package/dist/createMutation.d.ts +3 -0
  34. package/dist/createMutation.d.ts.map +1 -1
  35. package/dist/createMutation.js +87 -80
  36. package/dist/createMutation.js.map +1 -1
  37. package/dist/createQuery.d.ts +35 -2
  38. package/dist/createQuery.d.ts.map +1 -1
  39. package/dist/createQuery.js +104 -17
  40. package/dist/createQuery.js.map +1 -1
  41. package/dist/createReactorProvider.d.ts +158 -0
  42. package/dist/createReactorProvider.d.ts.map +1 -0
  43. package/dist/createReactorProvider.js +256 -0
  44. package/dist/createReactorProvider.js.map +1 -0
  45. package/dist/createSuspenseInfiniteQuery.d.ts +16 -9
  46. package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
  47. package/dist/createSuspenseInfiniteQuery.js +59 -27
  48. package/dist/createSuspenseInfiniteQuery.js.map +1 -1
  49. package/dist/createSuspenseQuery.d.ts +23 -2
  50. package/dist/createSuspenseQuery.d.ts.map +1 -1
  51. package/dist/createSuspenseQuery.js +68 -21
  52. package/dist/createSuspenseQuery.js.map +1 -1
  53. package/dist/defineDisplayReactor.d.ts +43 -0
  54. package/dist/defineDisplayReactor.d.ts.map +1 -0
  55. package/dist/defineDisplayReactor.js +42 -0
  56. package/dist/defineDisplayReactor.js.map +1 -0
  57. package/dist/defineReactor.d.ts +46 -72
  58. package/dist/defineReactor.d.ts.map +1 -1
  59. package/dist/defineReactor.js +11 -176
  60. package/dist/defineReactor.js.map +1 -1
  61. package/dist/defineReactorShared.d.ts +84 -0
  62. package/dist/defineReactorShared.d.ts.map +1 -0
  63. package/dist/defineReactorShared.js +139 -0
  64. package/dist/defineReactorShared.js.map +1 -0
  65. package/dist/hooks/createAuthHooks.d.ts +7 -0
  66. package/dist/hooks/createAuthHooks.d.ts.map +1 -1
  67. package/dist/hooks/createAuthHooks.js +180 -20
  68. package/dist/hooks/createAuthHooks.js.map +1 -1
  69. package/dist/hooks/useActorInfiniteQuery.d.ts +34 -6
  70. package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
  71. package/dist/hooks/useActorInfiniteQuery.js +54 -21
  72. package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
  73. package/dist/hooks/useActorMethod.d.ts +37 -4
  74. package/dist/hooks/useActorMethod.d.ts.map +1 -1
  75. package/dist/hooks/useActorMethod.js +201 -57
  76. package/dist/hooks/useActorMethod.js.map +1 -1
  77. package/dist/hooks/useActorMutation.d.ts +10 -7
  78. package/dist/hooks/useActorMutation.d.ts.map +1 -1
  79. package/dist/hooks/useActorMutation.js +14 -13
  80. package/dist/hooks/useActorMutation.js.map +1 -1
  81. package/dist/hooks/useActorQuery.d.ts +17 -4
  82. package/dist/hooks/useActorQuery.d.ts.map +1 -1
  83. package/dist/hooks/useActorQuery.js +30 -9
  84. package/dist/hooks/useActorQuery.js.map +1 -1
  85. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +15 -3
  86. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
  87. package/dist/hooks/useActorSuspenseInfiniteQuery.js +37 -17
  88. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
  89. package/dist/hooks/useActorSuspenseQuery.d.ts +2 -2
  90. package/dist/hooks/useActorSuspenseQuery.d.ts.map +1 -1
  91. package/dist/hooks/useActorSuspenseQuery.js +20 -9
  92. package/dist/hooks/useActorSuspenseQuery.js.map +1 -1
  93. package/dist/index.d.ts +4 -0
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +6 -0
  96. package/dist/index.js.map +1 -1
  97. package/dist/ownedAuthentication.d.ts +52 -0
  98. package/dist/ownedAuthentication.d.ts.map +1 -0
  99. package/dist/ownedAuthentication.js +49 -0
  100. package/dist/ownedAuthentication.js.map +1 -0
  101. package/dist/server.d.ts +21 -0
  102. package/dist/server.d.ts.map +1 -0
  103. package/dist/server.js +23 -0
  104. package/dist/server.js.map +1 -0
  105. package/dist/testing.d.ts +19 -0
  106. package/dist/testing.d.ts.map +1 -0
  107. package/dist/testing.js +19 -0
  108. package/dist/testing.js.map +1 -0
  109. package/dist/types.d.ts +416 -15
  110. package/dist/types.d.ts.map +1 -1
  111. package/dist/types.js +1 -1
  112. package/dist/utils.d.ts +159 -3
  113. package/dist/utils.d.ts.map +1 -1
  114. package/dist/utils.js +301 -1
  115. package/dist/utils.js.map +1 -1
  116. package/dist/validation.d.ts +12 -7
  117. package/dist/validation.d.ts.map +1 -1
  118. package/dist/validation.js +34 -15
  119. package/dist/validation.js.map +1 -1
  120. package/llms.txt +259 -33
  121. package/package.json +17 -5
  122. package/src/auth/auth-client-compat.ts +273 -0
  123. package/src/auth/authentication-manager.ts +918 -96
  124. package/src/auth/createIdentityAttributeHooks.ts +47 -21
  125. package/src/auth/identity-attributes-manager.ts +100 -5
  126. package/src/auth/identity-attributes.ts +75 -0
  127. package/src/auth/local-ii-probe.ts +29 -3
  128. package/src/auth/types.ts +49 -6
  129. package/src/createActorHooks.ts +20 -32
  130. package/src/createInfiniteQuery.ts +120 -28
  131. package/src/createMutation.ts +161 -178
  132. package/src/createQuery.ts +164 -32
  133. package/src/createReactorProvider.ts +365 -0
  134. package/src/createSuspenseInfiniteQuery.ts +93 -43
  135. package/src/createSuspenseQuery.ts +102 -32
  136. package/src/defineDisplayReactor.ts +62 -0
  137. package/src/defineReactor.ts +81 -263
  138. package/src/defineReactorShared.ts +268 -0
  139. package/src/hooks/createAuthHooks.ts +206 -24
  140. package/src/hooks/useActorInfiniteQuery.ts +122 -49
  141. package/src/hooks/useActorMethod.ts +295 -92
  142. package/src/hooks/useActorMutation.ts +23 -23
  143. package/src/hooks/useActorQuery.ts +43 -10
  144. package/src/hooks/useActorSuspenseInfiniteQuery.ts +93 -50
  145. package/src/hooks/useActorSuspenseQuery.ts +30 -15
  146. package/src/index.ts +8 -0
  147. package/src/ownedAuthentication.ts +81 -0
  148. package/src/server.ts +23 -0
  149. package/src/testing.ts +18 -0
  150. package/src/types.ts +463 -14
  151. package/src/utils.ts +387 -3
  152. package/src/validation.ts +43 -19
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # @ic-reactor/react
2
2
 
3
+ > **AI coding agents:** read [`llms.txt`](./llms.txt) in this package
4
+ > (`node_modules/@ic-reactor/react/llms.txt`) before writing code with it. It
5
+ > is written for the installed version and lists the patterns to use and the
6
+ > mistakes to avoid.
7
+
3
8
  React bindings for IC Reactor. This package re-exports everything from
4
9
  `@ic-reactor/core` and adds hook factories, auth hooks, direct reactor hooks,
5
10
  and reusable query or mutation factories built around TanStack Query.
@@ -10,19 +15,80 @@ and reusable query or mutation factories built around TanStack Query.
10
15
  pnpm add @ic-reactor/react @icp-sdk/core @tanstack/react-query
11
16
 
12
17
  # Optional: Internet Identity login helpers
13
- pnpm add @icp-sdk/auth@^8
18
+ pnpm add @icp-sdk/auth@^10
14
19
  ```
15
20
 
16
- > **npm needs an override to install this set.** Every published
17
- > `@icp-sdk/auth` peers `@icp-sdk/core@^5`, while this package needs `^6`, so a
18
- > strict `npm install` fails with `ERESOLVE`. The metadata is stale rather than
19
- > the versions being incompatible — auth v8 runs against core v6, and this
20
- > repository's own suite exercises that combination. pnpm and yarn install it
21
- > as-is; for npm, add:
21
+ It needs `@tanstack/react-query` 5.90.2 or later and React 18 or later. CI runs
22
+ this package's tests at those minimums (`pnpm verify:peer-floors`).
23
+
24
+ ## Which `@icp-sdk/auth` to install
25
+
26
+ The peer range is `^8.0.0 || ^10.0.0`. **v10 is the one to install.** It is the
27
+ first release whose peer is `@icp-sdk/core@^6` — the version this package needs —
28
+ so a strict `npm install` resolves it with no `overrides` block.
29
+
30
+ **v9 is deliberately excluded.** It peers `@icp-sdk/core@^5`, so it reintroduces
31
+ the resolution failure v10 fixes.
32
+
33
+ **v8 still works**, and this repository's real-client suite runs against both
34
+ majors. On npm it still needs the override, because its peer metadata is stale rather
35
+ than the versions being incompatible — auth v8 runs against core v6:
36
+
37
+ ```json
38
+ { "overrides": { "@icp-sdk/auth": { "@icp-sdk/core": "$@icp-sdk/core" } } }
39
+ ```
40
+
41
+ ### What changes if you move from v8 to v10
42
+
43
+ IC Reactor keeps one options contract across both majors and translates at the
44
+ boundary, so `identityProvider`, `derivationOrigin`, `windowOpenerFeatures`,
45
+ `transport` and `openIdProvider` are written the same way either way. One
46
+ difference in `identityProvider`: v10 names a provider by its authorize URL
47
+ and the canister that mints its delegations, so a URL you set yourself needs
48
+ `internetIdentityId` on v10 as well. IC Reactor pairs the mainnet URL and its
49
+ own local default with the right canister, and throws for any other URL that
50
+ has none, instead of guessing one. Four
51
+ things genuinely have no v10 equivalent, and IC Reactor warns once on each
52
+ rather than forwarding an option the client ignores:
53
+
54
+ | Option | On v10 |
55
+ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
56
+ | `storage` | Dropped. Credentials moved behind `credentialStorage`, whose store also generates identities and holds a delegation alongside each key, so an `AuthClientStorage` cannot be adapted into one. Pass a pre-built `authClient` to keep a custom store. |
57
+ | `keyType` | Dropped. The credential store decides the key type. |
58
+ | `idleOptions` | Dropped. The idle timeout belongs to the identity provider canister; pass `maxTimeToIdle` to `login()` instead. |
59
+ | `identity` | Dropped from constructor options. The agent signs as the session. |
60
+
61
+ Two options exist only on v10: `maxTimeToIdle` on `login()` and
62
+ `disableBrowserActivity` on the client. IC Reactor forwards them to a v10 client
63
+ and drops them on v8 with a one-time warning, since v8 has no equivalent.
64
+
65
+ A v10 client also tells IC Reactor when the session changes in another tab of
66
+ the origin. `AuthenticationManager` follows it: a sign-out in another tab signs
67
+ this tab out, and a sign-in there as another account is adopted here, with no
68
+ call in this tab. A v8 tab notices only when it checks the session again.
69
+
70
+ One difference is security-relevant and warns unconditionally: **`targets` on
71
+ `login()` is ignored by v10.** v8 forwards it to restrict the delegation to named
72
+ canisters; v10 removed it and scopes a session at the identity provider instead.
73
+ A v10 client will hand you a delegation broader than a `targets` list asks for.
74
+ Pin `@icp-sdk/auth` to `^8` if you depend on canister-scoped delegations.
75
+
76
+ > **Support scope.** The real-client suite
77
+ > (`tests/auth/internet-identity-integration.test.ts` and
78
+ > `tests/auth/auth-client-lifecycle.test.ts`) runs the actual `AuthClient`
79
+ > under v10 and under v8. A fake Internet Identity answers both
80
+ > sign-in protocols (`icrc34_delegation` and `ii_session_delegation`). A fake
81
+ > replica certifies v10's mint and revoke calls
82
+ > (`app_prepare_delegation`, `app_get_delegation`, `app_revoke_session`) and
83
+ > checks request signatures the way a replica does, so v10's own minting agent
84
+ > verifies them unchanged.
22
85
  >
23
- > ```json
24
- > { "overrides": { "@icp-sdk/auth": { "@icp-sdk/core": "$@icp-sdk/core" } } }
25
- > ```
86
+ > v10 makes those calls through an agent of its own. Off mainnet, IC Reactor
87
+ > gives that agent the replica your app already uses and has it fetch the
88
+ > network's root key, since certificates from a local replica or testnet cannot
89
+ > be checked against mainnet's. A root key you passed as `agentOptions.rootKey`
90
+ > goes to that agent instead, as your app's own agent keeps it. The suite covers that path. What it does not
91
+ > cover is a deployed Internet Identity canister.
26
92
 
27
93
  `@icp-sdk/auth` is an optional peer. `AuthenticationManager` reaches it through a
28
94
  literal `import("@icp-sdk/auth/client")`, so Vite, Rollup and webpack code-split
@@ -115,6 +181,13 @@ export function App() {
115
181
 
116
182
  ## Main APIs
117
183
 
184
+ - `defineReactor(...)` for one-call setup: the `QueryClient`, `ClientManager`,
185
+ a `Reactor`, its actor hooks and the Internet Identity hooks together;
186
+ `defineDisplayReactor(...)` takes the same options and builds a
187
+ `DisplayReactor` instead
188
+ - `createReactorProvider(factory)` for a server-rendered app: a provider that
189
+ builds the reactors once per mounted tree (so once per request on a server)
190
+ and a `useReactor()` hook that returns them fully typed
118
191
  - `createActorHooks(reactor)` for per-canister hooks like `useActorQuery` and
119
192
  `useActorMutation`
120
193
  - `createAuthHooks(authentication)` for `useAuth`, `useAgentState`, and
@@ -130,13 +203,63 @@ export function App() {
130
203
  ## Choosing the Right Pattern
131
204
 
132
205
  - Use `createActorHooks` for the simplest component-first integration.
206
+ - Use `createReactorProvider` when the app renders on a server (Next.js, any
207
+ React SSR) or a part of the UI needs reactors of its own: module-scope
208
+ reactors are right only for a client-only app.
133
209
  - Use query and mutation factories when you also need loader, action, service,
134
210
  or test usage through `.fetch()`, `.prefetch()`, `.execute()`, `.invalidate()`,
135
211
  `.getCacheData()`, or `.setData()`.
136
- - Use `DisplayReactor` when you want UI-friendly values such as strings instead
137
- of `bigint` or `Principal`.
212
+ - Use `DisplayReactor` (or `defineDisplayReactor`) when you want UI-friendly
213
+ values such as strings instead of `bigint` or `Principal`. It adds zod to the
214
+ bundle; see [Bundle Size](#bundle-size).
138
215
  - Use generated hooks from `@ic-reactor/vite-plugin` or `@ic-reactor/cli` when
139
216
  you have larger canisters or frequent `.did` changes.
217
+ - When a query's arguments are not known yet, pass `skipToken` (re-exported
218
+ from TanStack Query) in their place: `args: owner ? [owner] : skipToken` in
219
+ `useActorQuery`, `getArgs: skipToken` in `useActorInfiniteQuery`, or
220
+ `getBalance(owner ? [owner] : skipToken).useQuery()` on a query factory. The
221
+ query waits without calling the canister. Placeholder args with `enabled`,
222
+ or a `!`, are not needed. The suspense variants do not take it.
223
+ - Call a method that changes state through a mutation (`useActorMutation`,
224
+ `useActorMethod`, `createMutation`), never a query hook or factory. A query
225
+ runs its method again on every refetch (mount, window focus, reconnect,
226
+ invalidation), and an update method executes each time. Without a `retry` of
227
+ its own, a query of an update method retries only a `SysTransient`
228
+ rejection, which proves the call never ran, so a lost response is not
229
+ executed twice; see
230
+ [Update Methods in Queries](https://ic-reactor.b3pay.net/v3/framework/queries#update-methods-in-queries).
231
+
232
+ ## Bundle Size
233
+
234
+ What each setup path adds to a browser bundle, measured with esbuild 0.28
235
+ (minified ESM, `@ic-reactor/core` bundled). The peers `react`,
236
+ `@tanstack/react-query` and `@icp-sdk/*` are left out, since an app ships them
237
+ either way; so is `@icp-sdk/auth`, which loads as its own chunk on first use.
238
+
239
+ | Setup | Minified | Gzipped | zod |
240
+ | ---------------------------------------------------------------- | -------: | ------: | :-: |
241
+ | `createActorHooks` + `Reactor` + `ClientManager` | 31 kB | 9.8 kB | no |
242
+ | … + `AuthenticationManager` + `createAuthHooks` | 52 kB | 14.9 kB | no |
243
+ | … + `IdentityAttributesManager` + `createIdentityAttributeHooks` | 58 kB | 16.7 kB | no |
244
+ | `createActorHooks` + `DisplayReactor` + `ClientManager` | 129 kB | 37.3 kB | yes |
245
+ | `defineDisplayReactor` | 158 kB | 45.4 kB | yes |
246
+ | `defineReactor` | 158 kB | 45.4 kB | yes |
247
+
248
+ `DisplayReactor` builds its codecs on zod's classic API, which does not
249
+ tree-shake. That is about 85 kB minified (24 kB gzipped) of every row marked
250
+ "yes"; an app that already bundles zod for its own code pays it once.
251
+
252
+ `defineReactor` costs as much as `defineDisplayReactor` today, even without
253
+ `display`: its deprecated `display: true` option can still build a
254
+ `DisplayReactor`, so the class and zod stay in the bundle. When that option is
255
+ removed at the next major, `defineReactor` drops to about 60 kB minified,
256
+ 17.6 kB gzipped. Until then, an app that wants the smallest bundle and has no
257
+ use for `DisplayReactor` sets up with `createActorHooks(new Reactor(...))` and
258
+ `createAuthHooks` (see [Internet Identity](#internet-identity)).
259
+
260
+ Both `define*` functions also include the identity-attribute code (about 6 kB
261
+ minified, 1.8 kB gzipped) whether or not the app calls `useIdentityAttributes`,
262
+ because it is part of the object they return.
140
263
 
141
264
  ## Factory Example
142
265
 
@@ -150,6 +273,8 @@ export const getProfile = createSuspenseQueryFactory(backend, {
150
273
 
151
274
  export const updateProfile = createMutation(backend, {
152
275
  functionName: "update_profile",
276
+ // Every get_profile query this factory made, whatever the args
277
+ invalidateQueries: [getProfile],
153
278
  onCanisterError: (err) => console.error("Canister Err variant:", err.code),
154
279
  })
155
280
  ```
@@ -161,17 +286,25 @@ const profileQuery = getProfile(["alice"])
161
286
  const { data } = profileQuery.useSuspenseQuery()
162
287
 
163
288
  // Prefetch before navigating (fire-and-forget)
164
- profileQuery.prefetch()
289
+ void profileQuery.prefetch()
165
290
 
166
- // Optimistic update
291
+ // Write into the cache
167
292
  profileQuery.setData({ id: "alice", name: "Alice" })
168
293
 
169
- // Mutation with cache invalidation
294
+ // Mutation with extra invalidation: a query object, a query factory, a
295
+ // `{ functionName, args? }` method of the reactor, or a query key
170
296
  const mutation = updateProfile.useMutation({
171
- invalidateQueries: [profileQuery.getQueryKey()],
297
+ invalidateQueries: [{ functionName: "list_profiles" }],
172
298
  })
173
299
  ```
174
300
 
301
+ An `invalidateQueries` entry of `createMutation`, `useActorMutation` and
302
+ `useActorMethod` is a query object, a query factory (every query it returns),
303
+ a `{ functionName, args? }` method of the mutation's reactor, whose name and
304
+ args are type-checked, or a query key. The invalidation is awaited before
305
+ `onSuccess`. Query keys start with the canister ID, so a hand-written
306
+ `["get_profile"]` matches nothing.
307
+
175
308
  ## Internet Identity
176
309
 
177
310
  `defineReactor` wires up Internet Identity for you — `useAuth`,
@@ -180,24 +313,36 @@ alongside the actor hooks:
180
313
 
181
314
  ```tsx
182
315
  // src/reactor.ts
316
+ import { defineReactor } from "@ic-reactor/react"
317
+ import { canisterId, idlFactory, type _SERVICE } from "./declarations/backend"
318
+
183
319
  export const { useActorQuery, useAuth, useIdentityAttributes, authentication } =
184
320
  defineReactor<_SERVICE>({
185
321
  name: "backend",
186
322
  idlFactory,
323
+ canisterId,
187
324
  auth: {
188
325
  // Required when the app is served from more than one origin, so every
189
326
  // origin resolves to the same principal.
190
327
  derivationOrigin: "https://app.example.com",
191
- // The default signs the user out and reloads after 10 minutes idle.
192
- idleOptions: { disableIdle: true },
193
328
  },
194
329
  })
195
330
  ```
196
331
 
332
+ Idle handling depends on the installed major. On v8 the client signs the user
333
+ out and reloads the page after 10 minutes idle unless `auth` carries
334
+ `idleOptions: { disableIdle: true }`. v10 drops `idleOptions` with a warning and
335
+ leaves the idle limit to the identity provider: pass `maxTimeToIdle` to
336
+ `login()` to set it, and `disableBrowserActivity: true` in `auth` if only
337
+ requests should count as activity.
338
+
197
339
  Auth options are forwarded to the underlying `@icp-sdk/auth` client:
198
340
  `identityProvider`, `derivationOrigin`, `windowOpenerFeatures`,
199
- `openIdProvider`, `storage`, `keyType`, `idleOptions`, `identity`, and
200
- `transport`. Only the `"google" | "apple" | "microsoft"` aliases are accepted
341
+ `openIdProvider` and `transport` on either major, `storage`, `keyType`,
342
+ `idleOptions` and `identity` on v8 only, and `disableBrowserActivity` on v10
343
+ only. An option the installed major lacks is dropped with a one-time warning
344
+ (see [Which `@icp-sdk/auth` to install](#which-icp-sdkauth-to-install)). Only
345
+ the `"google" | "apple" | "microsoft"` aliases are accepted
201
346
  for `openIdProvider`; any other value is dropped, since raw issuer URLs are
202
347
  only meaningful on `requestOpenIdAttributes`, where they scope the keys.
203
348
 
@@ -248,6 +393,7 @@ If your bundler cannot resolve the optional peer at all, construct the client
248
393
  yourself and inject it — IC Reactor then never imports `@icp-sdk/auth`:
249
394
 
250
395
  ```ts
396
+ import { AuthenticationManager } from "@ic-reactor/react"
251
397
  import { AuthClient } from "@icp-sdk/auth/client"
252
398
 
253
399
  const authentication = new AuthenticationManager({
@@ -268,6 +414,12 @@ verification-error page. From `release-2026-03-23` the II frontend moved out of
268
414
  the canister, so no local build past that point can be used for sign-in — pin an
269
415
  older release in `dfx.json`.
270
416
 
417
+ Pinning only helps `@icp-sdk/auth` v8: v10 signs in through calls II gained
418
+ after its frontend left the canister, so no build both serves `/authorize` and
419
+ supports v10 sessions. With v10, serve an II frontend separately and pass its
420
+ authorize URL as `identityProvider`, with `internetIdentityId` set to the local
421
+ II canister that mints its delegations.
422
+
271
423
  An inconclusive probe — the canister unreachable, or answering no `http_request`
272
424
  — does not throw: it keeps `/authorize`, because a diagnostic that blocks a
273
425
  login that might have worked is worse than the failure it explains. To override
@@ -277,9 +429,9 @@ takes the path as its third argument.
277
429
  ## Identity Attributes / OpenID email and profile values
278
430
 
279
431
  Identity attributes use a dedicated `IdentityAttributesManager`, with React
280
- bindings created by `createIdentityAttributeHooks`. Requires `@icp-sdk/auth` v8 —
281
- the peer range is `^8.0.0`, and the v7 compatibility path was removed in 3.12.0.
282
- v8 takes the nonce as a thunk (`() => Promise<Uint8Array>`); IC Reactor accepts
432
+ bindings created by `createIdentityAttributeHooks`. Requires `@icp-sdk/auth` v8
433
+ or v10. The peer range is `^8.0.0 || ^10.0.0`, and the v7 compatibility path was
434
+ removed in 3.12.0. Both take the nonce as a thunk (`() => Promise<Uint8Array>`); IC Reactor accepts
283
435
  either a value or a callback and adapts it, but the callback form is what
284
436
  preserves the user gesture (see below).
285
437
 
@@ -297,7 +449,11 @@ await requestOpenIdAttributes({
297
449
 
298
450
  // ❌ gesture is gone by the time the window would open
299
451
  const nonce = await backend.callMethod({ functionName: "register_begin" })
300
- await requestOpenIdAttributes({ nonce, openIdProvider: "google", keys })
452
+ await requestOpenIdAttributes({
453
+ nonce,
454
+ openIdProvider: "google",
455
+ keys: ["email", "name"],
456
+ })
301
457
  ```
302
458
 
303
459
  ```tsx
@@ -366,6 +522,8 @@ cached result for a caller-scoped method (`get_my_balance`, a deposit address,
366
522
 
367
523
  ```tsx
368
524
  // ❌ Shared by every request on the server
525
+ import { defineReactor } from "@ic-reactor/react"
526
+
369
527
  export const app = defineReactor<_SERVICE>({
370
528
  name: "backend",
371
529
  idlFactory,
@@ -374,30 +532,113 @@ export const app = defineReactor<_SERVICE>({
374
532
  ```
375
533
 
376
534
  ```tsx
377
- // ✅ Per request: nothing is shared between users
535
+ // ✅ Per request: nothing is shared between users.
536
+ // In a server component this resolves to the `react-server` entry: the core
537
+ // classes are there, the hooks are not.
538
+ import { ClientManager, Reactor } from "@ic-reactor/react"
539
+ import { QueryClient } from "@tanstack/react-query"
540
+
378
541
  export default async function Page() {
379
- const app = defineReactor<_SERVICE>({
542
+ const clientManager = new ClientManager({ queryClient: new QueryClient() })
543
+ const reactor = new Reactor<_SERVICE>({
380
544
  name: "backend",
545
+ clientManager,
381
546
  idlFactory,
382
547
  canisterId,
383
- queryClient: new QueryClient(),
384
548
  })
385
549
 
386
- const data = await app.reactor.fetchQuery({ functionName: "get_my_profile" })
550
+ const data = await reactor.fetchQuery({ functionName: "get_my_profile" })
387
551
  return <Profile data={data} />
388
552
  }
389
553
  ```
390
554
 
555
+ Client components need their reactors built inside the React tree too.
556
+ `createReactorProvider` builds them once per mounted provider, in a `useState`
557
+ initializer, and a server render is a tree of its own, so each request gets its
558
+ own reactors, cache and `AuthenticationManager`. `useReactor()` returns what
559
+ the factory built with its full type, so the hooks on it keep their generic
560
+ signatures:
561
+
562
+ ```tsx
563
+ // src/reactor.tsx
564
+ "use client"
565
+ import { createReactorProvider, defineReactor } from "@ic-reactor/react"
566
+
567
+ export const { ReactorProvider, useReactor } = createReactorProvider(() =>
568
+ defineReactor<_SERVICE>({ name: "backend", idlFactory, canisterId })
569
+ )
570
+ ```
571
+
572
+ ```tsx
573
+ // app/layout.tsx (a server component) renders
574
+ // <ReactorProvider>{children}</ReactorProvider>, and a client component
575
+ // below it takes its hooks from useReactor():
576
+ export function Profile() {
577
+ const { useActorQuery } = useReactor()
578
+ const { data } = useActorQuery({ functionName: "get_my_profile" })
579
+ return <h1>{data?.name}</h1>
580
+ }
581
+ ```
582
+
583
+ The factory can return a record (`{ backend, ledger }`, read with
584
+ `useReactor("ledger")`), reactors and managers built by hand, or query and
585
+ mutation objects. It receives the provider's props, read once per mount; a new
586
+ `key` builds a new value. The provider also renders a `QueryClientProvider` for
587
+ the value's QueryClient, so `useQueryClient()`, React Query Devtools and a
588
+ `HydrationBoundary` below it use the cache the hooks fill. Below it, that
589
+ provider takes the place of an outer `QueryClientProvider`; pass
590
+ `{ queryClientProvider: false }` to keep your own.
591
+
592
+ A suspense hook below the provider may suspend its first render: the provider
593
+ reuses the value that render built when React renders it again. A provider that
594
+ a transition mounts (`startTransition`, a client-side navigation) can be built
595
+ again on each retry, so wrap its suspending components in a `<Suspense>`
596
+ boundary inside the provider.
597
+
598
+ When the tree unmounts, the provider disposes each `AuthenticationManager`
599
+ built for the value (in the factory, or later by a `defineReactor` result in
600
+ it), releasing the Internet Identity client it built: a v10 client keeps
601
+ listening to the page until it is disposed, so each remount would otherwise
602
+ leave one behind. A manager built elsewhere and passed in, such as an app-wide
603
+ one, is left alone. A provider you write yourself has to do the same from its
604
+ cleanup:
605
+
606
+ ```tsx
607
+ import { useEffect, useState } from "react"
608
+
609
+ const [value] = useState(createReactorContext)
610
+ useEffect(() => () => value.authentication.dispose(), [value])
611
+ ```
612
+
613
+ `dispose()` only forgets the client, and the next sign-in builds a new one, so
614
+ this is safe under StrictMode, which runs the cleanup and the effect again on
615
+ the same managers. A client passed in as `authClient` is left alone.
616
+
391
617
  Two further constraints on the App Router specifically:
392
618
 
393
619
  - Hooks are client-only, like every React hook — call them from a `"use client"`
394
- module. A server component may import `Reactor` / `ClientManager` and make
395
- imperative calls; that path works.
620
+ module. A server component, server action or route handler resolves
621
+ `@ic-reactor/react` to its `react-server` entry, which Next.js (Turbopack and
622
+ webpack) selects through the export condition of that name. That entry exports
623
+ everything `@ic-reactor/core` does — `Reactor`, `DisplayReactor`,
624
+ `ClientManager`, the error classes and utilities — plus the validation helpers
625
+ (`mapValidationErrors`, `getFieldError`, …), and nothing that imports React.
626
+ Importing a hook, `defineReactor`, `createActorHooks`, a query or mutation
627
+ factory, or the auth classes there fails `next build` with "Export
628
+ defineReactor doesn't exist in target module"; hooks belong in a
629
+ `"use client"` module, and server code calls the reactor itself
630
+ (`reactor.fetchQuery()`, `reactor.callMethod()`) where client code would use a
631
+ factory's `.fetch()` or `.execute()`. TypeScript does not read the condition,
632
+ so the editor does not flag it first. A server-component bundler that ignores
633
+ `react-server` loads the full entry and rejects its hooks: import from
634
+ `@ic-reactor/core` there, and list it in your own `package.json`, since a
635
+ transitive dependency does not resolve under pnpm.
396
636
  - Hooks bind to their reactor's own `QueryClient` rather than to a
397
637
  `QueryClientProvider`, so `HydrationBoundary` prefetch does not feed them
398
- unless the provider's client _is_ that reactor's client. Next.js also
399
- evaluates a shared module twice on the server (the RSC and SSR graphs), so a
400
- module-scope reactor is two different instances there.
638
+ unless the provider's client _is_ that reactor's client, as it is below
639
+ `createReactorProvider`'s provider. Next.js also evaluates a shared module
640
+ twice on the server (the RSC and SSR graphs), so a module-scope reactor is
641
+ two different instances there.
401
642
 
402
643
  If none of that applies — a client-only SPA — module-scope reactors are exactly
403
644
  right and none of this is a concern.
@@ -407,15 +648,64 @@ right and none of this is a concern.
407
648
  Every object returned by `createQuery`, `createSuspenseQuery`, and their
408
649
  factory variants exposes:
409
650
 
410
- | Method | Description |
411
- | ----------------------------------- | ----------------------------------------------------------------------------------------------- |
412
- | `fetch()` | Cache-first fetch — returns data, populates cache. Use in route loaders. |
413
- | `prefetch()` | Fire-and-forget cache warm-up. Use on hover or before navigation. |
414
- | `invalidate()` | Invalidates the cache entry (triggers refetch if query is mounted). |
415
- | `getQueryKey()` | Returns the TanStack Query key for this query. |
416
- | `getCacheData(select?)` | Read directly from cache without fetching. Returns `undefined` if not cached. |
417
- | `setData(updater)` | Write raw data into the cache. Accepts a value or updater function. Use for optimistic updates. |
418
- | `useQuery()` / `useSuspenseQuery()` | React hook for the query. |
651
+ | Method | Description |
652
+ | ----------------------------------- | ------------------------------------------------------------------------------------------ |
653
+ | `fetch()` | Cache-first fetch — returns data, populates cache. Use in route loaders. |
654
+ | `prefetch()` | Fire-and-forget cache warm-up. Use on hover or before navigation. |
655
+ | `invalidate()` | Invalidates the cache entry (triggers refetch if query is mounted). |
656
+ | `getQueryKey()` | Returns the TanStack Query key for this query. |
657
+ | `getCacheData(select?)` | Read directly from cache without fetching. Returns `undefined` if not cached. |
658
+ | `setData(updater)` | Write raw data into the cache. Accepts a value or updater function. |
659
+ | `optimisticUpdate(updater)` | Cancel the fetch in flight, write `updater(cached)`, resolve with `{ rollback() }`. |
660
+ | `cancel()` | Cancel this query's fetch in flight; the entry keeps its previous value. |
661
+ | `reset()` | Reset this entry to its initial state; a mounted hook refetches, a suspense hook suspends. |
662
+ | `useQuery()` / `useSuspenseQuery()` | React hook for the query. |
663
+
664
+ A sign-in or sign-out while `fetch()` is in flight does not reject it: the
665
+ previous identity's answer is dropped, and `fetch()` runs again for the new
666
+ identity and resolves with that answer. `prefetch()` runs again too, and still
667
+ never rejects: when that run succeeds, the cache holds the new identity's
668
+ answer by the time it resolves. The infinite query factories' `fetch()` behaves
669
+ the same (they have no `prefetch()`).
670
+
671
+ `optimisticUpdate`, `cancel` and `reset` act on the query's own entry only, and
672
+ the infinite query objects have them too, over their `{ pages, pageParams }`.
673
+ An optimistic update is three lines of mutation config:
674
+
675
+ ```tsx
676
+ import { createMutation, createQueryFactory } from "@ic-reactor/react"
677
+ import { backend } from "./reactor"
678
+
679
+ const getPost = createQueryFactory(backend, { functionName: "get_post" })
680
+ const likePost = createMutation(backend, { functionName: "like_post" })
681
+
682
+ // In a component
683
+ const { mutate } = likePost.useMutation({
684
+ onMutate: ([postId]) =>
685
+ getPost([postId]).optimisticUpdate((post) => ({
686
+ ...post,
687
+ likes: post.likes + 1n,
688
+ })),
689
+ onError: (_error, _args, update) => update?.rollback(),
690
+ onSettled: (_data, _error, [postId]) => getPost([postId]).invalidate(),
691
+ })
692
+ ```
693
+
694
+ The updater gets the raw, typed value and is not called when nothing is
695
+ cached. Refetch once the mutation settles, as `onSettled` does here: the fetch
696
+ `optimisticUpdate` cancels may be a refetch an invalidation or a sign-in
697
+ started. `rollback()` does nothing after a sign-in or sign-out, because the
698
+ value it kept was the previous principal's.
699
+
700
+ Use `backend.queryClient` rather than `useQueryClient()` when you need the
701
+ QueryClient itself: the hooks bind to the reactor's client, and
702
+ `useQueryClient()` throws without the optional `QueryClientProvider`.
703
+
704
+ The function a factory variant returns (`createQueryFactory`,
705
+ `createSuspenseQueryFactory`, `createInfiniteQueryFactory`,
706
+ `createSuspenseInfiniteQueryFactory`) also has `getQueryKey()`, the key prefix
707
+ every query it returns shares, and `invalidate()`, which invalidates all of
708
+ them whatever their args.
419
709
 
420
710
  ## Canister Error Handling
421
711
 
@@ -425,6 +715,9 @@ via `onCanisterError`. This callback is supported on both `createMutation` and
425
715
  the direct `useActorMutation` hook:
426
716
 
427
717
  ```tsx
718
+ import { createMutation } from "@ic-reactor/react"
719
+ import { backend, useActorMutation } from "./reactor"
720
+
428
721
  // Via createActorHooks
429
722
  const { mutate } = useActorMutation({
430
723
  functionName: "transfer",
@@ -448,8 +741,10 @@ const transferMutation = createMutation(backend, {
448
741
 
449
742
  ## Re-exports
450
743
 
451
- `@ic-reactor/react` re-exports the core runtime, so you can import these from a
452
- single package:
744
+ `@ic-reactor/react` re-exports the core runtime, so client and server code can
745
+ import these from a single package. A React Server Component resolves the
746
+ package's `react-server` entry, which has them but none of the hooks; see
747
+ [Server-Side Rendering](#server-side-rendering):
453
748
 
454
749
  - `ClientManager`
455
750
  - `Reactor`
@@ -457,6 +752,77 @@ single package:
457
752
  - `CallError`
458
753
  - `CanisterError`
459
754
  - `ValidationError`
755
+ - `formatTokenAmount` and `parseTokenAmount`, which convert a ledger's base
756
+ units to and from decimal text exactly (see
757
+ [Token Amounts](../core/README.md#token-amounts)); do not use `Number` for
758
+ either
759
+ - `isPrincipalText`, which checks a principal a person typed without a `try`
760
+ around `Principal.fromText`
761
+
762
+ The main entry also re-exports TanStack Query's `skipToken` (and its
763
+ `SkipToken` type), the same symbol `@tanstack/react-query` exports.
764
+
765
+ ## Testing
766
+
767
+ `@ic-reactor/react/testing` re-exports `@ic-reactor/core/testing`, so an app
768
+ that depends on this package alone can test its components against a fake
769
+ replica instead of a `Reactor` stub:
770
+
771
+ ```tsx
772
+ import { afterEach, beforeEach, expect, it } from "vitest"
773
+ import { renderHook, waitFor } from "@testing-library/react"
774
+ import { ClientManager, Reactor, createActorHooks } from "@ic-reactor/react"
775
+ import {
776
+ createTestCanister,
777
+ installFakeReplica,
778
+ type FakeReplica,
779
+ } from "@ic-reactor/react/testing"
780
+ import { QueryClient } from "@tanstack/react-query"
781
+ import { idlFactory, type _SERVICE } from "./declarations/backend"
782
+
783
+ const BACKEND = "bkyz2-fmaaa-aaaaa-qaaaq-cai"
784
+ let replica: FakeReplica
785
+
786
+ beforeEach(() => {
787
+ replica = installFakeReplica({
788
+ canisters: {
789
+ [BACKEND]: createTestCanister<_SERVICE>(idlFactory, {
790
+ balance: () => 42n,
791
+ }),
792
+ },
793
+ })
794
+ })
795
+ // Also when the test fails, so the next test gets a fetch of its own.
796
+ afterEach(() => replica.restore())
797
+
798
+ it("reads the balance", async () => {
799
+ // Built after the fake is installed, and pointed at it.
800
+ const reactor = new Reactor<_SERVICE>({
801
+ clientManager: new ClientManager({
802
+ queryClient: new QueryClient(),
803
+ agentOptions: { host: replica.host },
804
+ }),
805
+ name: "backend",
806
+ canisterId: BACKEND,
807
+ idlFactory,
808
+ })
809
+ const { useActorQuery } = createActorHooks(reactor)
810
+
811
+ const { result } = renderHook(() =>
812
+ useActorQuery({ functionName: "balance" })
813
+ )
814
+
815
+ await waitFor(() => expect(result.current.data).toBe(42n))
816
+ })
817
+ ```
818
+
819
+ A reactor built at module scope, such as one from `defineReactor`, builds its
820
+ agent when its module is imported: install the fake first, then import the
821
+ component under test dynamically. With no `host` on either side, its
822
+ `ClientManager` and the fake both use the page's origin in jsdom, so they
823
+ meet. The
824
+ [Testing guide](https://ic-reactor.b3pay.net/v3/guides/testing) shows how, and
825
+ how to test as a signed-in user.
460
826
 
461
827
  ## See Also
462
828