@ic-reactor/react 3.12.5 → 4.0.0-beta.1

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 (139) hide show
  1. package/README.md +236 -423
  2. package/dist/index.d.ts +259 -12
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +470 -17
  5. package/dist/index.js.map +1 -1
  6. package/llms.txt +82 -54
  7. package/package.json +12 -28
  8. package/src/index.tsx +611 -0
  9. package/dist/auth/authentication-manager.d.ts +0 -123
  10. package/dist/auth/authentication-manager.d.ts.map +0 -1
  11. package/dist/auth/authentication-manager.js +0 -767
  12. package/dist/auth/authentication-manager.js.map +0 -1
  13. package/dist/auth/constants.d.ts +0 -24
  14. package/dist/auth/constants.d.ts.map +0 -1
  15. package/dist/auth/constants.js +0 -24
  16. package/dist/auth/constants.js.map +0 -1
  17. package/dist/auth/createIdentityAttributeHooks.d.ts +0 -14
  18. package/dist/auth/createIdentityAttributeHooks.d.ts.map +0 -1
  19. package/dist/auth/createIdentityAttributeHooks.js +0 -106
  20. package/dist/auth/createIdentityAttributeHooks.js.map +0 -1
  21. package/dist/auth/identity-attributes-manager.d.ts +0 -26
  22. package/dist/auth/identity-attributes-manager.d.ts.map +0 -1
  23. package/dist/auth/identity-attributes-manager.js +0 -107
  24. package/dist/auth/identity-attributes-manager.js.map +0 -1
  25. package/dist/auth/identity-attributes.d.ts +0 -19
  26. package/dist/auth/identity-attributes.d.ts.map +0 -1
  27. package/dist/auth/identity-attributes.js +0 -170
  28. package/dist/auth/identity-attributes.js.map +0 -1
  29. package/dist/auth/index.d.ts +0 -8
  30. package/dist/auth/index.d.ts.map +0 -1
  31. package/dist/auth/index.js +0 -8
  32. package/dist/auth/index.js.map +0 -1
  33. package/dist/auth/local-ii-probe.d.ts +0 -46
  34. package/dist/auth/local-ii-probe.d.ts.map +0 -1
  35. package/dist/auth/local-ii-probe.js +0 -102
  36. package/dist/auth/local-ii-probe.js.map +0 -1
  37. package/dist/auth/types.d.ts +0 -179
  38. package/dist/auth/types.d.ts.map +0 -1
  39. package/dist/auth/types.js +0 -2
  40. package/dist/auth/types.js.map +0 -1
  41. package/dist/createActorHooks.d.ts +0 -52
  42. package/dist/createActorHooks.d.ts.map +0 -1
  43. package/dist/createActorHooks.js +0 -17
  44. package/dist/createActorHooks.js.map +0 -1
  45. package/dist/createInfiniteQuery.d.ts +0 -144
  46. package/dist/createInfiniteQuery.d.ts.map +0 -1
  47. package/dist/createInfiniteQuery.js +0 -174
  48. package/dist/createInfiniteQuery.js.map +0 -1
  49. package/dist/createMutation.d.ts +0 -30
  50. package/dist/createMutation.d.ts.map +0 -1
  51. package/dist/createMutation.js +0 -192
  52. package/dist/createMutation.js.map +0 -1
  53. package/dist/createQuery.d.ts +0 -30
  54. package/dist/createQuery.d.ts.map +0 -1
  55. package/dist/createQuery.js +0 -117
  56. package/dist/createQuery.js.map +0 -1
  57. package/dist/createSuspenseInfiniteQuery.d.ts +0 -147
  58. package/dist/createSuspenseInfiniteQuery.d.ts.map +0 -1
  59. package/dist/createSuspenseInfiniteQuery.js +0 -177
  60. package/dist/createSuspenseInfiniteQuery.js.map +0 -1
  61. package/dist/createSuspenseQuery.d.ts +0 -25
  62. package/dist/createSuspenseQuery.d.ts.map +0 -1
  63. package/dist/createSuspenseQuery.js +0 -111
  64. package/dist/createSuspenseQuery.js.map +0 -1
  65. package/dist/defineReactor.d.ts +0 -125
  66. package/dist/defineReactor.d.ts.map +0 -1
  67. package/dist/defineReactor.js +0 -180
  68. package/dist/defineReactor.js.map +0 -1
  69. package/dist/hooks/createAuthHooks.d.ts +0 -43
  70. package/dist/hooks/createAuthHooks.d.ts.map +0 -1
  71. package/dist/hooks/createAuthHooks.js +0 -131
  72. package/dist/hooks/createAuthHooks.js.map +0 -1
  73. package/dist/hooks/index.d.ts +0 -21
  74. package/dist/hooks/index.d.ts.map +0 -1
  75. package/dist/hooks/index.js +0 -24
  76. package/dist/hooks/index.js.map +0 -1
  77. package/dist/hooks/useActorInfiniteQuery.d.ts +0 -39
  78. package/dist/hooks/useActorInfiniteQuery.d.ts.map +0 -1
  79. package/dist/hooks/useActorInfiniteQuery.js +0 -56
  80. package/dist/hooks/useActorInfiniteQuery.js.map +0 -1
  81. package/dist/hooks/useActorMethod.d.ts +0 -115
  82. package/dist/hooks/useActorMethod.d.ts.map +0 -1
  83. package/dist/hooks/useActorMethod.js +0 -250
  84. package/dist/hooks/useActorMethod.js.map +0 -1
  85. package/dist/hooks/useActorMutation.d.ts +0 -48
  86. package/dist/hooks/useActorMutation.d.ts.map +0 -1
  87. package/dist/hooks/useActorMutation.js +0 -69
  88. package/dist/hooks/useActorMutation.js.map +0 -1
  89. package/dist/hooks/useActorQuery.d.ts +0 -32
  90. package/dist/hooks/useActorQuery.d.ts.map +0 -1
  91. package/dist/hooks/useActorQuery.js +0 -46
  92. package/dist/hooks/useActorQuery.js.map +0 -1
  93. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +0 -39
  94. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +0 -1
  95. package/dist/hooks/useActorSuspenseInfiniteQuery.js +0 -56
  96. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +0 -1
  97. package/dist/hooks/useActorSuspenseQuery.d.ts +0 -32
  98. package/dist/hooks/useActorSuspenseQuery.d.ts.map +0 -1
  99. package/dist/hooks/useActorSuspenseQuery.js +0 -47
  100. package/dist/hooks/useActorSuspenseQuery.js.map +0 -1
  101. package/dist/types.d.ts +0 -270
  102. package/dist/types.d.ts.map +0 -1
  103. package/dist/types.js +0 -5
  104. package/dist/types.js.map +0 -1
  105. package/dist/utils.d.ts +0 -51
  106. package/dist/utils.d.ts.map +0 -1
  107. package/dist/utils.js +0 -105
  108. package/dist/utils.js.map +0 -1
  109. package/dist/validation.d.ts +0 -131
  110. package/dist/validation.d.ts.map +0 -1
  111. package/dist/validation.js +0 -125
  112. package/dist/validation.js.map +0 -1
  113. package/src/auth/authentication-manager.ts +0 -860
  114. package/src/auth/constants.ts +0 -32
  115. package/src/auth/createIdentityAttributeHooks.ts +0 -143
  116. package/src/auth/identity-attributes-manager.ts +0 -131
  117. package/src/auth/identity-attributes.ts +0 -270
  118. package/src/auth/index.ts +0 -7
  119. package/src/auth/local-ii-probe.ts +0 -147
  120. package/src/auth/types.ts +0 -200
  121. package/src/createActorHooks.ts +0 -220
  122. package/src/createInfiniteQuery.ts +0 -578
  123. package/src/createMutation.ts +0 -341
  124. package/src/createQuery.ts +0 -237
  125. package/src/createSuspenseInfiniteQuery.ts +0 -601
  126. package/src/createSuspenseQuery.ts +0 -234
  127. package/src/defineReactor.ts +0 -324
  128. package/src/hooks/createAuthHooks.ts +0 -189
  129. package/src/hooks/index.ts +0 -103
  130. package/src/hooks/useActorInfiniteQuery.ts +0 -205
  131. package/src/hooks/useActorMethod.ts +0 -507
  132. package/src/hooks/useActorMutation.ts +0 -205
  133. package/src/hooks/useActorQuery.ts +0 -124
  134. package/src/hooks/useActorSuspenseInfiniteQuery.ts +0 -205
  135. package/src/hooks/useActorSuspenseQuery.ts +0 -132
  136. package/src/index.ts +0 -23
  137. package/src/types.ts +0 -499
  138. package/src/utils.ts +0 -121
  139. package/src/validation.ts +0 -202
package/README.md CHANGED
@@ -1,466 +1,279 @@
1
1
  # @ic-reactor/react
2
2
 
3
- React bindings for IC Reactor. This package re-exports everything from
4
- `@ic-reactor/core` and adds hook factories, auth hooks, direct reactor hooks,
5
- and reusable query or mutation factories built around TanStack Query.
3
+ > **ic-reactor 4 is a prerelease.** `4.0.0-beta.1` is published under npm's
4
+ > `beta` dist-tag, and `latest` stays 3.x until 4.0 GA (milestone 1,
5
+ > [#790](https://github.com/B3Pay/ic-reactor/issues/790)).
6
6
 
7
- ## Install
7
+ [![npm version](https://img.shields.io/npm/v/@ic-reactor/react.svg)](https://www.npmjs.com/package/@ic-reactor/react)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
9
 
9
- ```bash
10
- pnpm add @ic-reactor/react @icp-sdk/core @tanstack/react-query
10
+ The React bindings of ic-reactor 4: three `'use client'` exports over a client
11
+ made by [`@ic-reactor/core`](../core/README.md).
11
12
 
12
- # Optional: Internet Identity login helpers
13
- pnpm add @icp-sdk/auth@^8
14
- ```
13
+ | Export | What it is |
14
+ | ----------------- | ------------------------------------------------------------------------ |
15
+ | `ReactorProvider` | Gives a tree one client, and TanStack Query that client's `QueryClient`. |
16
+ | `useClient` | The client of the nearest provider, for the caller this render shows. |
17
+ | `useAuth` | Who calls (`status`, `principal`), with `signIn` and `signOut`. |
15
18
 
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:
22
- >
23
- > ```json
24
- > { "overrides": { "@icp-sdk/auth": { "@icp-sdk/core": "$@icp-sdk/core" } } }
25
- > ```
26
-
27
- `@icp-sdk/auth` is an optional peer. `AuthenticationManager` reaches it through a
28
- literal `import("@icp-sdk/auth/client")`, so Vite, Rollup and webpack code-split
29
- it into its own async chunk. That chunk is never fetched unless something
30
- touches authentication, and its bytes are dropped from the output entirely in
31
- apps that never reference the class.
32
-
33
- Bundlers still **resolve** that specifier while building the module graph, which
34
- happens before any tree-shaking — so a missing peer cannot simply be optimized
35
- away. The import therefore sits inside a `try` block, which webpack-family
36
- bundlers treat as declaring an optional dependency: with the peer absent the
37
- build succeeds and prints one warning,
38
- `Module not found: Can't resolve '@icp-sdk/auth/client'`. Only the login paths
39
- are affected, and they throw an actionable error if they are ever called.
40
-
41
- Install the peer to remove the warning, or silence it with
42
- [`ignoreWarnings`](https://webpack.js.org/configuration/other-options/#ignorewarnings):
43
-
44
- ```js
45
- // webpack.config.js / next.config.js (webpack)
46
- ignoreWarnings: [{ module: /@ic-reactor\/react/, message: /@icp-sdk\/auth/ }]
47
- ```
19
+ There is no hook that wraps `useQuery` or `useMutation`: read and write
20
+ canisters with TanStack Query's own hooks and the options the client builds.
21
+ The package never re-exports `@ic-reactor/core`, so every name has one import
22
+ path. The guide for both packages ships in core:
23
+ `node_modules/@ic-reactor/core/llms.txt`.
48
24
 
49
- ## Quick Start
25
+ ## Install
50
26
 
51
- ```tsx
52
- // src/reactor.ts
53
- import { ClientManager, Reactor, createActorHooks } from "@ic-reactor/react"
54
- import { QueryClient } from "@tanstack/react-query"
55
- import { canisterId, idlFactory, type _SERVICE } from "./declarations/backend"
27
+ ```bash
28
+ npm install @ic-reactor/core@beta @ic-reactor/react@beta @tanstack/react-query
29
+ ```
56
30
 
57
- export const queryClient = new QueryClient()
31
+ Peers: `react` 18 or newer, `@tanstack/react-query` 5, and `@ic-reactor/core` at
32
+ exactly this package's version (the two are released together).
58
33
 
59
- export const clientManager = new ClientManager({
60
- queryClient,
61
- })
34
+ ## Provide a client
62
35
 
63
- export const backend = new Reactor<_SERVICE>({
64
- clientManager,
65
- idlFactory,
66
- name: "backend",
67
- // `dfx` writes this alongside the idlFactory. Required — omit it only when
68
- // the vite-plugin injects an `ic_env` cookie for this canister; outside that
69
- // flow the constructor throws.
70
- canisterId,
71
- })
36
+ `ReactorProvider` takes a factory, not a client, and calls it once for each
37
+ mounted provider. When it unmounts, it disposes the client only if that factory
38
+ call created it. Both ways of writing the factory are fine:
72
39
 
73
- export const {
74
- useActorQuery,
75
- useActorMutation,
76
- useActorSuspenseQuery,
77
- useActorMethod,
78
- } = createActorHooks(backend)
79
- ```
40
+ - **The provider owns the client.** A factory that creates it,
41
+ `client={() => createClient({ ... })}`, gives each mounted provider a client
42
+ of its own, and the provider disposes it when it unmounts. Use this in an
43
+ app that renders on a server: the factory runs once per request.
44
+ - **The app owns the client.** A client created at module scope, one per tab
45
+ and used outside React too, is handed over as `client={() => client}`. The
46
+ provider borrows it and never disposes it, however many times it mounts and
47
+ unmounts: the app decides when it ends.
80
48
 
81
- ```tsx
82
- // src/App.tsx
83
- import { QueryClientProvider } from "@tanstack/react-query"
84
- import { queryClient, useActorMethod, useActorQuery } from "./reactor"
85
-
86
- function Greeting() {
87
- const { data, isPending } = useActorQuery({
88
- functionName: "greet",
89
- args: ["World"],
90
- })
91
-
92
- if (isPending) return <p>Loading...</p>
93
- return <p>{data}</p>
94
- }
49
+ `createClient` does no work until the client is used, so the same line runs in
50
+ a server render (anonymous, no auth built) and in the browser (signed in).
95
51
 
96
- function Increment() {
97
- const { call, isPending } = useActorMethod({ functionName: "increment" })
52
+ ```tsx
53
+ "use client"
98
54
 
99
- return (
100
- <button disabled={isPending} onClick={() => call([])}>
101
- {isPending ? "Updating..." : "Increment"}
102
- </button>
103
- )
104
- }
55
+ import { createClient } from "@ic-reactor/core"
56
+ import { ReactorProvider } from "@ic-reactor/react"
57
+ import { AuthClient } from "@icp-sdk/auth/client"
58
+ import type { ReactNode } from "react"
105
59
 
106
- export function App() {
60
+ export function Providers({ children }: { children: ReactNode }) {
107
61
  return (
108
- <QueryClientProvider client={queryClient}>
109
- <Greeting />
110
- <Increment />
111
- </QueryClientProvider>
62
+ <ReactorProvider
63
+ client={() =>
64
+ createClient({ network: "ic", auth: () => new AuthClient() })
65
+ }
66
+ >
67
+ {children}
68
+ </ReactorProvider>
112
69
  )
113
70
  }
114
71
  ```
115
72
 
116
- ## Main APIs
117
-
118
- - `createActorHooks(reactor)` for per-canister hooks like `useActorQuery` and
119
- `useActorMutation`
120
- - `createAuthHooks(authentication)` for `useAuth`, `useAgentState`, and
121
- `useUserPrincipal`
122
- - `createIdentityAttributeHooks(identityAttributes)` for signed identity
123
- attribute requests
124
- - direct reactor hooks like `useReactorQuery` when you want to pass the reactor
125
- instance at call time
126
- - factory helpers like `createQuery`, `createSuspenseQuery`,
127
- `createInfiniteQuery`, `createSuspenseInfiniteQuery`, and `createMutation`
128
- when the same operation must work both inside and outside React
129
-
130
- ## Choosing the Right Pattern
131
-
132
- - Use `createActorHooks` for the simplest component-first integration.
133
- - Use query and mutation factories when you also need loader, action, service,
134
- or test usage through `.fetch()`, `.prefetch()`, `.execute()`, `.invalidate()`,
135
- `.getCacheData()`, or `.setData()`.
136
- - Use `DisplayReactor` when you want UI-friendly values such as strings instead
137
- of `bigint` or `Principal`.
138
- - Use generated hooks from `@ic-reactor/vite-plugin` or `@ic-reactor/cli` when
139
- you have larger canisters or frequent `.did` changes.
140
-
141
- ## Factory Example
142
-
143
- ```ts
144
- import { createSuspenseQueryFactory, createMutation } from "@ic-reactor/react"
145
- import { backend } from "./reactor"
146
-
147
- export const getProfile = createSuspenseQueryFactory(backend, {
148
- functionName: "get_profile",
149
- })
150
-
151
- export const updateProfile = createMutation(backend, {
152
- functionName: "update_profile",
153
- onCanisterError: (err) => console.error("Canister Err variant:", err.code),
154
- })
155
- ```
156
-
157
- ```tsx
158
- const profileQuery = getProfile(["alice"])
159
-
160
- // React component
161
- const { data } = profileQuery.useSuspenseQuery()
162
-
163
- // Prefetch before navigating (fire-and-forget)
164
- profileQuery.prefetch()
165
-
166
- // Optimistic update
167
- profileQuery.setData({ id: "alice", name: "Alice" })
168
-
169
- // Mutation with cache invalidation
170
- const mutation = updateProfile.useMutation({
171
- invalidateQueries: [profileQuery.getQueryKey()],
172
- })
173
- ```
174
-
175
- ## Internet Identity
176
-
177
- `defineReactor` wires up Internet Identity for you — `useAuth`,
178
- `useUserPrincipal`, `useAgentState` and `useIdentityAttributes` come back
179
- alongside the actor hooks:
180
-
181
- ```tsx
182
- // src/reactor.ts
183
- export const { useActorQuery, useAuth, useIdentityAttributes, authentication } =
184
- defineReactor<_SERVICE>({
185
- name: "backend",
186
- idlFactory,
187
- auth: {
188
- // Required when the app is served from more than one origin, so every
189
- // origin resolves to the same principal.
190
- derivationOrigin: "https://app.example.com",
191
- // The default signs the user out and reloads after 10 minutes idle.
192
- idleOptions: { disableIdle: true },
193
- },
194
- })
195
- ```
196
-
197
- Auth options are forwarded to the underlying `@icp-sdk/auth` client:
198
- `identityProvider`, `derivationOrigin`, `windowOpenerFeatures`,
199
- `openIdProvider`, `storage`, `keyType`, `idleOptions`, `identity`, and
200
- `transport`. Only the `"google" | "apple" | "microsoft"` aliases are accepted
201
- for `openIdProvider`; any other value is dropped, since raw issuer URLs are
202
- only meaningful on `requestOpenIdAttributes`, where they scope the keys.
203
-
204
- Pass `authentication` from one reactor into another to share a single session
205
- across canisters. That reactor adopts the manager's `ClientManager` so sign-in
206
- updates the agent it calls through, so pass either `authentication` or `auth` —
207
- not both.
73
+ Render it at the root of the part of the app that calls canisters. In a
74
+ framework that renders Server Components, keep the factory in a client module
75
+ like this one: a function cannot cross from a Server Component into a client
76
+ one, so `<Providers>` is what the server layout renders.
208
77
 
209
- Set up manually when you need more control:
78
+ A client the app owns, in a browser-only app:
210
79
 
211
80
  ```tsx
212
- // src/auth.ts
213
- import {
214
- AuthenticationManager,
215
- IdentityAttributesManager,
216
- createAuthHooks,
217
- createIdentityAttributeHooks,
218
- } from "@ic-reactor/react"
219
- import { clientManager } from "./reactor"
220
-
221
- const authentication = new AuthenticationManager({ clientManager })
222
- export const { useAuth, useAgentState, useUserPrincipal } =
223
- createAuthHooks(authentication)
224
-
225
- const identityAttributes = new IdentityAttributesManager(authentication)
226
- export const { useIdentityAttributes } =
227
- createIdentityAttributeHooks(identityAttributes)
228
- ```
229
-
230
- `useAuth()` calls `authentication.prepareClient()` on mount. Outside React, do
231
- it yourself during startup — it preloads the auth module so `login()` can open
232
- the identity provider window synchronously inside a click handler, which is
233
- what browser popup blockers require:
81
+ "use client"
234
82
 
235
- ```ts
236
- // once, at startup — awaits the dynamic import and builds the AuthClient
237
- await authentication.prepareClient()
238
-
239
- // later, inside the click handler — no await before signIn(), so the popup
240
- // still counts as user-initiated
241
- button.onclick = () => authentication.login()
242
- ```
243
-
244
- `authentication.getPreparedClient()` returns the already-built client
245
- synchronously, or `undefined` when the module has not loaded yet.
246
-
247
- If your bundler cannot resolve the optional peer at all, construct the client
248
- yourself and inject it — IC Reactor then never imports `@icp-sdk/auth`:
249
-
250
- ```ts
83
+ import { createClient } from "@ic-reactor/core"
84
+ import { ReactorProvider } from "@ic-reactor/react"
251
85
  import { AuthClient } from "@icp-sdk/auth/client"
86
+ import type { ReactNode } from "react"
252
87
 
253
- const authentication = new AuthenticationManager({
254
- clientManager,
255
- authClient: new AuthClient(),
256
- })
257
- ```
258
-
259
- ### Local Internet Identity
260
-
261
- On a local replica the provider URL is resolved for you. `prepareClient()`
262
- queries the local Internet Identity canister's `http_request` to see what the
263
- installed build actually serves, then targets `/authorize` (release-2026-01-05 …
264
- release-2026-03-16) or the legacy `/#authorize` (up to release-2025-03-07, which
265
- also logs a console warning). If the build serves neither, `login()` throws an
266
- actionable error rather than opening a popup onto the gateway's
267
- verification-error page. From `release-2026-03-23` the II frontend moved out of
268
- the canister, so no local build past that point can be used for sign-in — pin an
269
- older release in `dfx.json`.
270
-
271
- An inconclusive probe — the canister unreachable, or answering no `http_request`
272
- — does not throw: it keeps `/authorize`, because a diagnostic that blocks a
273
- login that might have worked is worse than the failure it explains. To override
274
- the URL yourself, `localInternetIdentityProvider(port, canisterId?, authorizePath?)`
275
- takes the path as its third argument.
276
-
277
- ## Identity Attributes / OpenID email and profile values
278
-
279
- 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
283
- either a value or a callback and adapts it, but the callback form is what
284
- preserves the user gesture (see below).
285
-
286
- **Pass the nonce as a callback.** Awaiting your backend before calling
287
- `requestOpenIdAttributes` ends the user gesture, and the browser then blocks
288
- the Internet Identity window:
289
-
290
- ```tsx
291
- // ✅ window opens immediately, nonce resolves while the user is in II
292
- await requestOpenIdAttributes({
293
- nonce: () => backend.callMethod({ functionName: "register_begin" }),
294
- openIdProvider: "google",
295
- keys: ["email", "name"],
88
+ // One client for this tab, also used outside React.
89
+ export const client = createClient({
90
+ network: "ic",
91
+ auth: () => new AuthClient(),
296
92
  })
297
93
 
298
- // ❌ gesture is gone by the time the window would open
299
- const nonce = await backend.callMethod({ functionName: "register_begin" })
300
- await requestOpenIdAttributes({ nonce, openIdProvider: "google", keys })
94
+ export function Providers({ children }: { children: ReactNode }) {
95
+ return <ReactorProvider client={() => client}>{children}</ReactorProvider>
96
+ }
301
97
  ```
302
98
 
99
+ Who owns a client follows from when it was created, not from where the factory
100
+ is written. A getter that creates the shared client on its first call
101
+ (`client ??= createClient(...)`) makes the first provider that calls it the
102
+ owner, and that provider disposes it when it unmounts. It can lose the client
103
+ even before that: when React throws away the render that created it (a
104
+ Suspense boundary above the provider that suspends), the client stays queued
105
+ for disposal at garbage collection until the next render gets it back, so a
106
+ collection while the fallback shows disposes the client the page then mounts
107
+ (see [Life of the client](#life-of-the-client)). Create a shared client
108
+ eagerly, as above. In development, a provider whose factory hands back a client
109
+ that an earlier factory call created and no provider has committed yet, as a
110
+ lazily shared getter does, logs a warning that names the fix (under
111
+ `StrictMode`, which calls the factory twice, on the first render); and a
112
+ provider that is given a client which is already disposed logs an error.
113
+
114
+ ## Who is signed in
115
+
303
116
  ```tsx
304
- // src/RegisterWithOpenIdProvider.tsx
305
- import { useIdentityAttributes } from "./auth"
306
- import { backend } from "./reactor"
307
-
308
- function RegisterWithOpenIdProvider() {
309
- const {
310
- requestOpenIdAttributes,
311
- attributes,
312
- isRequestingAttributes,
313
- attributeError,
314
- } = useIdentityAttributes()
315
-
316
- async function handleProviderLogin() {
317
- const result = await requestOpenIdAttributes({
318
- nonce: () => backend.callMethod({ functionName: "register_begin" }),
319
- openIdProvider: "microsoft",
320
- keys: ["email", "name"],
321
- // Optional: any window.open() features string, e.g. to size the popup
322
- windowOpenerFeatures: "width=500,height=640",
323
- })
324
-
325
- console.log(result.decodedAttributes.email)
326
- console.log(result.decodedAttributes.name)
327
-
328
- await backend.callMethod({
329
- functionName: "register_finish",
330
- args: [
331
- {
332
- data: result.signedAttributes.data,
333
- signature: result.signedAttributes.signature,
334
- },
335
- ],
336
- })
337
- }
117
+ import { useAuth } from "@ic-reactor/react"
338
118
 
339
- return (
340
- <button disabled={isRequestingAttributes} onClick={handleProviderLogin}>
341
- {attributes?.decodedAttributes.email ??
342
- attributeError?.message ??
343
- "Continue with provider"}
119
+ export function SessionButton() {
120
+ const { status, principal, signIn, signOut } = useAuth()
121
+ return status === "signed-in" ? (
122
+ <button onClick={() => signOut().catch(console.error)}>
123
+ Sign out {principal}
344
124
  </button>
125
+ ) : (
126
+ <button onClick={() => signIn().catch(console.error)}>Sign in</button>
345
127
  )
346
128
  }
347
129
  ```
348
130
 
349
- Use a documented auth provider alias (`"google"`, `"apple"`, or `"microsoft"`)
350
- or the provider issuer URL your app expects for `openIdProvider`.
351
-
352
- Frontend decoded `email` and `name` values are for display only. Production flows
353
- must send `signedAttributes.data` and `signedAttributes.signature` to the backend
354
- or canister and verify the signature, nonce, origin, timestamp, and requested keys
355
- before trusting or storing the attributes.
356
-
357
- ## Server-Side Rendering
358
-
359
- **Build the reactor inside the request, not at module scope.**
360
-
361
- A reactor owns its `QueryClient`. On a server a module-scope reactor is created
362
- once per process and shared by every request, and query keys are
363
- `[canisterId, functionName, args]` — they do not include the caller. So a
364
- cached result for a caller-scoped method (`get_my_balance`, a deposit address,
365
- `my_profile`) is handed to whichever request asks next:
131
+ `status` is `"anonymous"`, `"signed-in"`, `"expired"` or `"signed-in-elsewhere"`,
132
+ and `principal` is the principal calls go out as: the anonymous principal
133
+ (`2vxsx-fae`) in every state but `"signed-in"`. A component that uses
134
+ `useAuth()` renders once for each change of status or principal, and never for
135
+ anything else; the object it returns is the same until one changes.
136
+
137
+ ## Keys follow the caller a render shows
138
+
139
+ Query keys carry the caller. Call `useClient()` in the body of each component
140
+ that builds keys or read options, on every render, and build them from what it
141
+ returns there, never from a client held at module scope. Do not keep what it
142
+ returns past the render: while a page hydrates it is a view for the anonymous
143
+ caller, and a view never moves on. An effect or a callback that uses it closes
144
+ over the value of its own render and lists it in its dependencies, as the
145
+ `react-hooks/exhaustive-deps` lint rule asks, so that it runs again with the
146
+ client once the page has hydrated. Kept from the hydrating render anywhere
147
+ else (`useState(client)`, `useRef(client)`, a `useMemo` or `useCallback` with
148
+ `[]`, a module variable), it stays that view: the component shows the
149
+ anonymous caller's data, a read it starts while the user is signed in is
150
+ cancelled (`caller_changed`), and a write through it still signs as the live
151
+ caller. Nothing warns about it. A nested `ReactorProvider` given it holds the
152
+ client the view was made over.
366
153
 
367
154
  ```tsx
368
- // ❌ Shared by every request on the server
369
- export const app = defineReactor<_SERVICE>({
370
- name: "backend",
371
- idlFactory,
372
- canisterId,
373
- })
374
- ```
375
-
376
- ```tsx
377
- // ✅ Per request: nothing is shared between users
378
- export default async function Page() {
379
- const app = defineReactor<_SERVICE>({
380
- name: "backend",
381
- idlFactory,
382
- canisterId,
383
- queryClient: new QueryClient(),
384
- })
385
-
386
- const data = await app.reactor.fetchQuery({ functionName: "get_my_profile" })
387
- return <Profile data={data} />
155
+ import { useClient } from "@ic-reactor/react"
156
+ import { useQuery } from "@tanstack/react-query"
157
+ import { actor, type Actor } from "./generated/icrc1"
158
+
159
+ export function Fee({ id }: { id: string }) {
160
+ const client = useClient()
161
+ const ledger = client.canister<Actor>(actor, { id })
162
+ const fee = useQuery(client.queryOptions(ledger, "icrc1_fee"))
163
+ return <span>{fee.data?.toString() ?? "…"}</span>
388
164
  }
389
165
  ```
390
166
 
391
- Two further constraints on the App Router specifically:
392
-
393
- - 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.
396
- - Hooks bind to their reactor's own `QueryClient` rather than to a
397
- `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.
401
-
402
- If none of that applies — a client-only SPA — module-scope reactors are exactly
403
- right and none of this is a concern.
404
-
405
- ## Query Result Methods
406
-
407
- Every object returned by `createQuery`, `createSuspenseQuery`, and their
408
- factory variants exposes:
409
-
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. |
419
-
420
- ## Canister Error Handling
421
-
422
- Canister methods can return `Result { Err: E }` variants. These are surfaced
423
- as `CanisterError` and can be handled separately from network or agent errors
424
- via `onCanisterError`. This callback is supported on both `createMutation` and
425
- the direct `useActorMutation` hook:
426
-
427
- ```tsx
428
- // Via createActorHooks
429
- const { mutate } = useActorMutation({
430
- functionName: "transfer",
431
- onCanisterError: (err, vars) => {
432
- // err.code — the Err variant key (e.g. "InsufficientFunds")
433
- // err.err — the typed Err value
434
- console.error(`Transfer failed: ${err.code}`, vars)
435
- },
436
- onError: (err) => {
437
- // Fires for ALL errors: canister Err variants, network failures, etc.
438
- console.error("Unexpected error", err)
439
- },
440
- })
441
-
442
- // Via createMutation factory
443
- const transferMutation = createMutation(backend, {
444
- functionName: "transfer",
445
- onCanisterError: (err) => toast.error(`${err.code}`),
446
- })
447
- ```
448
-
449
- ## Re-exports
450
-
451
- `@ic-reactor/react` re-exports the core runtime, so you can import these from a
452
- single package:
453
-
454
- - `ClientManager`
455
- - `Reactor`
456
- - `DisplayReactor`
457
- - `CallError`
458
- - `CanisterError`
459
- - `ValidationError`
460
-
461
- ## See Also
462
-
463
- - Docs: https://ic-reactor.b3pay.net/v3/packages/react
464
- - `@ic-reactor/core`: ../core/README.md
465
- - `@ic-reactor/vite-plugin`: ../vite-plugin/README.md
466
- - `@ic-reactor/cli`: ../cli/README.md
167
+ `useClient()` follows the client's caller, so a component that calls it renders
168
+ again on a sign-in, a switch of account and a sign-out, and builds the new
169
+ caller's keys; a change of status that leaves the caller as it is (a session
170
+ that expired, or one signed in elsewhere, both call anonymously) renders
171
+ nothing. It returns the provider's client object itself, except while a page
172
+ hydrates in a browser that holds a session (below). A client built with
173
+ `identity` keeps its caller for good and is always returned as it is.
174
+
175
+ ## On a server
176
+
177
+ - **One client per request.** A server renders each request as a tree of its
178
+ own, so a factory that creates the client runs once per request, and no
179
+ cache or caller is shared between two users. Never build the client at
180
+ module scope on a server: a module-scope client is for a browser-only app.
181
+ - **Anonymous while hydrating.** `useAuth()` is `anonymous` on a server and
182
+ while a page hydrates, even when the browser holds a session: the server has
183
+ none, and the HTML has to match. `useClient()` builds keys for that same
184
+ anonymous caller there: in a browser that holds a session (an `AuthClient`
185
+ reads a stored one synchronously), the hydrating render gets a view of the
186
+ client whose `queryKey`, `queryOptions`, `caller()` and `authState()` are
187
+ the anonymous caller's, so the page finds what the server prefetched and
188
+ dehydrated, matches its HTML, and sends nothing. Canisters, writes,
189
+ `signIn`, `signOut` and the `QueryClient` are the client's own, and a write
190
+ signs as the caller current when it runs. Right after hydrating, React
191
+ renders each component that calls `useClient()` or `useAuth()` again with
192
+ the session, and each read loads the user's keys once: until the user's data
193
+ arrives, a read shows its loading state, and a `useSuspenseQuery` read its
194
+ boundary's fallback. A tab whose caller
195
+ is anonymous anyway (no session, or one that expired or is signed in
196
+ elsewhere) renders no `useClient()` component again, and no `useAuth()`
197
+ component either unless its status differs. Show the same thing signed out
198
+ and while the session is read, and the page does not flicker into a
199
+ different layout.
200
+ - **Nothing runs on a server but the render.** The auth factory is never called,
201
+ nothing reads `window` or `localStorage`, and no effect runs, so no timer or
202
+ listener outlives the request.
203
+
204
+ ### What a signed-in reload costs
205
+
206
+ Three consequences of that move to the session are accepted, and tested:
207
+
208
+ - **A Suspense boundary still dehydrated below a component that renders with
209
+ the caller is rendered on the client.** When a component that calls
210
+ `useClient()` or `useAuth()` moves on to the session, the update reaches the
211
+ boundaries it renders. A boundary that has not hydrated yet, because its
212
+ lazy code is still loading or its streamed HTML has not arrived, is then
213
+ rendered on the client: it shows its fallback instead of
214
+ the server's HTML until its content is ready, and React 18 reports a
215
+ recoverable error. Its data is still the user's, and nothing is sent as
216
+ anyone else. A `useAuth()` component moves on, and costs the same, in a tab
217
+ whose session expired or is signed in elsewhere too. To keep the server's HTML, render such a boundary where no
218
+ component that renders with the caller sits above it (a `useAuth()` header
219
+ beside it is fine), or pass it in as `children`: a component's own update
220
+ does not render its `children` prop again.
221
+ - **A read the hydrating render built may run once and be cancelled.**
222
+ TanStack Query refetches stale data on mount, and an effect may fetch with
223
+ the hydrating render's options. Such a read is for the anonymous caller
224
+ while the user is current, so it is cancelled before anything is sent. A
225
+ key that holds data (the server's) keeps it as it was, and a fetch of it
226
+ resolves with that data. A key with none fails, with `kind` `"cancelled"`
227
+ and `code` `"caller_changed"`, until the anonymous caller is current again.
228
+ The hydrating render's own reads never show that error, but a `QueryCache`
229
+ `onError`, an effect that awaits the fetch and, for a `useSuspenseQuery`
230
+ read the server rendered without dehydrating its data, React's
231
+ `onRecoverableError` (as the reported error's `cause` on React 19) see it:
232
+ ignore `kind` `"cancelled"` there.
233
+ - **A `useSuspenseQuery` read needs a Suspense boundary above it, on React 18
234
+ and 19 alike.** The move to the session is a synchronous update, and a
235
+ `useSuspenseQuery` read with none of the user's data yet suspends it. With a
236
+ boundary above the read, the boundary shows its fallback until the user's
237
+ data arrives, then the data, and the rest of the page responds meanwhile.
238
+ With none, React 18 refuses the update ("A component suspended while
239
+ responding to synchronous input") and unmounts the root: a signed-in reload
240
+ renders nothing. React 19 keeps the server's HTML on screen, but until the
241
+ user's data arrives, however long the read and its retries take, a click or
242
+ any other update outside a transition commits nothing, and neither does a
243
+ transition that renders the reading component again. React expects a
244
+ boundary above any component that suspends anyway: put one there.
245
+
246
+ ## Life of the client
247
+
248
+ The provider disposes a client its factory created when it unmounts: the
249
+ client stops listening to the auth, disposes it, and clears the `QueryClient`.
250
+ React's development double-mount (`StrictMode` unmounts and mounts every
251
+ component at once) does not dispose a client in use, because the disposal is
252
+ scheduled for the next macrotask and the second mount cancels it. A client the
253
+ app owns is never disposed by a provider.
254
+
255
+ React runs no cleanup for a render it throws away before committing it, such
256
+ as a provider's first render below a Suspense boundary that suspends. A client
257
+ the factory built for such a render, whose auth a `useAuth()` below may
258
+ already have built, is disposed once that render's state is garbage collected
259
+ (through a `FinalizationRegistry`, in a browser): later than an unmount would,
260
+ but its auth and listeners do not outlive it. A client the factory returns
261
+ again, or that a mounted provider runs on, is never disposed this way, whoever
262
+ created it.
263
+
264
+ Only the factory of the first render is used: passing another function on a
265
+ later render does not rebuild the client. To replace the client, give the
266
+ provider another `key`.
267
+
268
+ When React shows a hidden `Activity` again after the client the provider owns
269
+ was disposed, the provider builds another one with the same factory. That
270
+ client starts with an empty cache: hiding a provider inside an `Activity` drops
271
+ everything it had fetched, because React cannot tell a hidden subtree from an
272
+ unmounted one when it cleans up. To keep the cache across hiding, render the
273
+ provider above the `Activity`, or give it a client the app owns.
274
+
275
+ ## 3.x
276
+
277
+ The released 3.x package, with its hook factories and auth hooks, is documented
278
+ at https://ic-reactor.b3pay.net/v3/packages/react. Its source and security fixes
279
+ live on the `main` branch.