@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.
- package/README.md +236 -423
- package/dist/index.d.ts +259 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +470 -17
- package/dist/index.js.map +1 -1
- package/llms.txt +82 -54
- package/package.json +12 -28
- package/src/index.tsx +611 -0
- package/dist/auth/authentication-manager.d.ts +0 -123
- package/dist/auth/authentication-manager.d.ts.map +0 -1
- package/dist/auth/authentication-manager.js +0 -767
- package/dist/auth/authentication-manager.js.map +0 -1
- package/dist/auth/constants.d.ts +0 -24
- package/dist/auth/constants.d.ts.map +0 -1
- package/dist/auth/constants.js +0 -24
- package/dist/auth/constants.js.map +0 -1
- package/dist/auth/createIdentityAttributeHooks.d.ts +0 -14
- package/dist/auth/createIdentityAttributeHooks.d.ts.map +0 -1
- package/dist/auth/createIdentityAttributeHooks.js +0 -106
- package/dist/auth/createIdentityAttributeHooks.js.map +0 -1
- package/dist/auth/identity-attributes-manager.d.ts +0 -26
- package/dist/auth/identity-attributes-manager.d.ts.map +0 -1
- package/dist/auth/identity-attributes-manager.js +0 -107
- package/dist/auth/identity-attributes-manager.js.map +0 -1
- package/dist/auth/identity-attributes.d.ts +0 -19
- package/dist/auth/identity-attributes.d.ts.map +0 -1
- package/dist/auth/identity-attributes.js +0 -170
- package/dist/auth/identity-attributes.js.map +0 -1
- package/dist/auth/index.d.ts +0 -8
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/index.js +0 -8
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/local-ii-probe.d.ts +0 -46
- package/dist/auth/local-ii-probe.d.ts.map +0 -1
- package/dist/auth/local-ii-probe.js +0 -102
- package/dist/auth/local-ii-probe.js.map +0 -1
- package/dist/auth/types.d.ts +0 -179
- package/dist/auth/types.d.ts.map +0 -1
- package/dist/auth/types.js +0 -2
- package/dist/auth/types.js.map +0 -1
- package/dist/createActorHooks.d.ts +0 -52
- package/dist/createActorHooks.d.ts.map +0 -1
- package/dist/createActorHooks.js +0 -17
- package/dist/createActorHooks.js.map +0 -1
- package/dist/createInfiniteQuery.d.ts +0 -144
- package/dist/createInfiniteQuery.d.ts.map +0 -1
- package/dist/createInfiniteQuery.js +0 -174
- package/dist/createInfiniteQuery.js.map +0 -1
- package/dist/createMutation.d.ts +0 -30
- package/dist/createMutation.d.ts.map +0 -1
- package/dist/createMutation.js +0 -192
- package/dist/createMutation.js.map +0 -1
- package/dist/createQuery.d.ts +0 -30
- package/dist/createQuery.d.ts.map +0 -1
- package/dist/createQuery.js +0 -117
- package/dist/createQuery.js.map +0 -1
- package/dist/createSuspenseInfiniteQuery.d.ts +0 -147
- package/dist/createSuspenseInfiniteQuery.d.ts.map +0 -1
- package/dist/createSuspenseInfiniteQuery.js +0 -177
- package/dist/createSuspenseInfiniteQuery.js.map +0 -1
- package/dist/createSuspenseQuery.d.ts +0 -25
- package/dist/createSuspenseQuery.d.ts.map +0 -1
- package/dist/createSuspenseQuery.js +0 -111
- package/dist/createSuspenseQuery.js.map +0 -1
- package/dist/defineReactor.d.ts +0 -125
- package/dist/defineReactor.d.ts.map +0 -1
- package/dist/defineReactor.js +0 -180
- package/dist/defineReactor.js.map +0 -1
- package/dist/hooks/createAuthHooks.d.ts +0 -43
- package/dist/hooks/createAuthHooks.d.ts.map +0 -1
- package/dist/hooks/createAuthHooks.js +0 -131
- package/dist/hooks/createAuthHooks.js.map +0 -1
- package/dist/hooks/index.d.ts +0 -21
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/index.js +0 -24
- package/dist/hooks/index.js.map +0 -1
- package/dist/hooks/useActorInfiniteQuery.d.ts +0 -39
- package/dist/hooks/useActorInfiniteQuery.d.ts.map +0 -1
- package/dist/hooks/useActorInfiniteQuery.js +0 -56
- package/dist/hooks/useActorInfiniteQuery.js.map +0 -1
- package/dist/hooks/useActorMethod.d.ts +0 -115
- package/dist/hooks/useActorMethod.d.ts.map +0 -1
- package/dist/hooks/useActorMethod.js +0 -250
- package/dist/hooks/useActorMethod.js.map +0 -1
- package/dist/hooks/useActorMutation.d.ts +0 -48
- package/dist/hooks/useActorMutation.d.ts.map +0 -1
- package/dist/hooks/useActorMutation.js +0 -69
- package/dist/hooks/useActorMutation.js.map +0 -1
- package/dist/hooks/useActorQuery.d.ts +0 -32
- package/dist/hooks/useActorQuery.d.ts.map +0 -1
- package/dist/hooks/useActorQuery.js +0 -46
- package/dist/hooks/useActorQuery.js.map +0 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +0 -39
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +0 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.js +0 -56
- package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +0 -1
- package/dist/hooks/useActorSuspenseQuery.d.ts +0 -32
- package/dist/hooks/useActorSuspenseQuery.d.ts.map +0 -1
- package/dist/hooks/useActorSuspenseQuery.js +0 -47
- package/dist/hooks/useActorSuspenseQuery.js.map +0 -1
- package/dist/types.d.ts +0 -270
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/dist/utils.d.ts +0 -51
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -105
- package/dist/utils.js.map +0 -1
- package/dist/validation.d.ts +0 -131
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -125
- package/dist/validation.js.map +0 -1
- package/src/auth/authentication-manager.ts +0 -860
- package/src/auth/constants.ts +0 -32
- package/src/auth/createIdentityAttributeHooks.ts +0 -143
- package/src/auth/identity-attributes-manager.ts +0 -131
- package/src/auth/identity-attributes.ts +0 -270
- package/src/auth/index.ts +0 -7
- package/src/auth/local-ii-probe.ts +0 -147
- package/src/auth/types.ts +0 -200
- package/src/createActorHooks.ts +0 -220
- package/src/createInfiniteQuery.ts +0 -578
- package/src/createMutation.ts +0 -341
- package/src/createQuery.ts +0 -237
- package/src/createSuspenseInfiniteQuery.ts +0 -601
- package/src/createSuspenseQuery.ts +0 -234
- package/src/defineReactor.ts +0 -324
- package/src/hooks/createAuthHooks.ts +0 -189
- package/src/hooks/index.ts +0 -103
- package/src/hooks/useActorInfiniteQuery.ts +0 -205
- package/src/hooks/useActorMethod.ts +0 -507
- package/src/hooks/useActorMutation.ts +0 -205
- package/src/hooks/useActorQuery.ts +0 -124
- package/src/hooks/useActorSuspenseInfiniteQuery.ts +0 -205
- package/src/hooks/useActorSuspenseQuery.ts +0 -132
- package/src/index.ts +0 -23
- package/src/types.ts +0 -499
- package/src/utils.ts +0 -121
- package/src/validation.ts +0 -202
package/README.md
CHANGED
|
@@ -1,466 +1,279 @@
|
|
|
1
1
|
# @ic-reactor/react
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/@ic-reactor/react)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
##
|
|
25
|
+
## Install
|
|
50
26
|
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
queryClient,
|
|
61
|
-
})
|
|
34
|
+
## Provide a client
|
|
62
35
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
97
|
-
|
|
52
|
+
```tsx
|
|
53
|
+
"use client"
|
|
98
54
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
60
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
107
61
|
return (
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
78
|
+
A client the app owns, in a browser-only app:
|
|
210
79
|
|
|
211
80
|
```tsx
|
|
212
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
`
|
|
364
|
-
|
|
365
|
-
`
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
- `
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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.
|