@ic-reactor/react 3.13.0 → 4.0.0-beta.2
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 +237 -787
- package/dist/index.d.ts +260 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +471 -23
- package/dist/index.js.map +1 -1
- package/llms.txt +82 -278
- package/package.json +11 -39
- package/src/index.tsx +612 -0
- package/dist/auth/auth-client-compat.d.ts +0 -122
- package/dist/auth/auth-client-compat.d.ts.map +0 -1
- package/dist/auth/auth-client-compat.js +0 -162
- package/dist/auth/auth-client-compat.js.map +0 -1
- package/dist/auth/authentication-manager.d.ts +0 -405
- package/dist/auth/authentication-manager.d.ts.map +0 -1
- package/dist/auth/authentication-manager.js +0 -1537
- 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 -122
- package/dist/auth/createIdentityAttributeHooks.js.map +0 -1
- package/dist/auth/identity-attributes-manager.d.ts +0 -27
- package/dist/auth/identity-attributes-manager.d.ts.map +0 -1
- package/dist/auth/identity-attributes-manager.js +0 -191
- 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 -227
- 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 -57
- package/dist/auth/local-ii-probe.d.ts.map +0 -1
- package/dist/auth/local-ii-probe.js +0 -121
- package/dist/auth/local-ii-probe.js.map +0 -1
- package/dist/auth/types.d.ts +0 -222
- 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 -41
- 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 -185
- package/dist/createInfiniteQuery.d.ts.map +0 -1
- package/dist/createInfiniteQuery.js +0 -198
- package/dist/createInfiniteQuery.js.map +0 -1
- package/dist/createMutation.d.ts +0 -33
- package/dist/createMutation.d.ts.map +0 -1
- package/dist/createMutation.js +0 -199
- package/dist/createMutation.js.map +0 -1
- package/dist/createQuery.d.ts +0 -63
- package/dist/createQuery.d.ts.map +0 -1
- package/dist/createQuery.js +0 -204
- package/dist/createQuery.js.map +0 -1
- package/dist/createReactorProvider.d.ts +0 -158
- package/dist/createReactorProvider.d.ts.map +0 -1
- package/dist/createReactorProvider.js +0 -256
- package/dist/createReactorProvider.js.map +0 -1
- package/dist/createSuspenseInfiniteQuery.d.ts +0 -154
- package/dist/createSuspenseInfiniteQuery.d.ts.map +0 -1
- package/dist/createSuspenseInfiniteQuery.js +0 -209
- package/dist/createSuspenseInfiniteQuery.js.map +0 -1
- package/dist/createSuspenseQuery.d.ts +0 -46
- package/dist/createSuspenseQuery.d.ts.map +0 -1
- package/dist/createSuspenseQuery.js +0 -158
- package/dist/createSuspenseQuery.js.map +0 -1
- package/dist/defineDisplayReactor.d.ts +0 -43
- package/dist/defineDisplayReactor.d.ts.map +0 -1
- package/dist/defineDisplayReactor.js +0 -42
- package/dist/defineDisplayReactor.js.map +0 -1
- package/dist/defineReactor.d.ts +0 -99
- package/dist/defineReactor.d.ts.map +0 -1
- package/dist/defineReactor.js +0 -15
- package/dist/defineReactor.js.map +0 -1
- package/dist/defineReactorShared.d.ts +0 -84
- package/dist/defineReactorShared.d.ts.map +0 -1
- package/dist/defineReactorShared.js +0 -139
- package/dist/defineReactorShared.js.map +0 -1
- package/dist/hooks/createAuthHooks.d.ts +0 -50
- package/dist/hooks/createAuthHooks.d.ts.map +0 -1
- package/dist/hooks/createAuthHooks.js +0 -291
- 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 -67
- package/dist/hooks/useActorInfiniteQuery.d.ts.map +0 -1
- package/dist/hooks/useActorInfiniteQuery.js +0 -89
- package/dist/hooks/useActorInfiniteQuery.js.map +0 -1
- package/dist/hooks/useActorMethod.d.ts +0 -148
- package/dist/hooks/useActorMethod.d.ts.map +0 -1
- package/dist/hooks/useActorMethod.js +0 -394
- package/dist/hooks/useActorMethod.js.map +0 -1
- package/dist/hooks/useActorMutation.d.ts +0 -51
- package/dist/hooks/useActorMutation.d.ts.map +0 -1
- package/dist/hooks/useActorMutation.js +0 -70
- package/dist/hooks/useActorMutation.js.map +0 -1
- package/dist/hooks/useActorQuery.d.ts +0 -45
- package/dist/hooks/useActorQuery.d.ts.map +0 -1
- package/dist/hooks/useActorQuery.js +0 -67
- package/dist/hooks/useActorQuery.js.map +0 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +0 -51
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +0 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.js +0 -76
- 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 -58
- package/dist/hooks/useActorSuspenseQuery.js.map +0 -1
- package/dist/ownedAuthentication.d.ts +0 -52
- package/dist/ownedAuthentication.d.ts.map +0 -1
- package/dist/ownedAuthentication.js +0 -49
- package/dist/ownedAuthentication.js.map +0 -1
- package/dist/server.d.ts +0 -21
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -23
- package/dist/server.js.map +0 -1
- package/dist/testing.d.ts +0 -19
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js +0 -19
- package/dist/testing.js.map +0 -1
- package/dist/types.d.ts +0 -671
- 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 -207
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -405
- package/dist/utils.js.map +0 -1
- package/dist/validation.d.ts +0 -136
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -144
- package/dist/validation.js.map +0 -1
- package/src/auth/auth-client-compat.ts +0 -273
- package/src/auth/authentication-manager.ts +0 -1682
- package/src/auth/constants.ts +0 -32
- package/src/auth/createIdentityAttributeHooks.ts +0 -169
- package/src/auth/identity-attributes-manager.ts +0 -226
- package/src/auth/identity-attributes.ts +0 -345
- package/src/auth/index.ts +0 -7
- package/src/auth/local-ii-probe.ts +0 -173
- package/src/auth/types.ts +0 -243
- package/src/createActorHooks.ts +0 -208
- package/src/createInfiniteQuery.ts +0 -670
- package/src/createMutation.ts +0 -324
- package/src/createQuery.ts +0 -369
- package/src/createReactorProvider.ts +0 -365
- package/src/createSuspenseInfiniteQuery.ts +0 -651
- package/src/createSuspenseQuery.ts +0 -304
- package/src/defineDisplayReactor.ts +0 -62
- package/src/defineReactor.ts +0 -142
- package/src/defineReactorShared.ts +0 -268
- package/src/hooks/createAuthHooks.ts +0 -371
- package/src/hooks/index.ts +0 -103
- package/src/hooks/useActorInfiniteQuery.ts +0 -278
- package/src/hooks/useActorMethod.ts +0 -710
- package/src/hooks/useActorMutation.ts +0 -205
- package/src/hooks/useActorQuery.ts +0 -157
- package/src/hooks/useActorSuspenseInfiniteQuery.ts +0 -248
- package/src/hooks/useActorSuspenseQuery.ts +0 -147
- package/src/index.ts +0 -31
- package/src/ownedAuthentication.ts +0 -81
- package/src/server.ts +0 -23
- package/src/testing.ts +0 -18
- package/src/types.ts +0 -948
- package/src/utils.ts +0 -505
- package/src/validation.ts +0 -226
package/README.md
CHANGED
|
@@ -1,832 +1,282 @@
|
|
|
1
1
|
# @ic-reactor/react
|
|
2
2
|
|
|
3
|
-
> **
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
> mistakes to avoid.
|
|
3
|
+
> **ic-reactor 4 is a prerelease.** `4.0.0-beta.2` 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)).
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
and reusable query or mutation factories built around TanStack Query.
|
|
7
|
+
[](https://www.npmjs.com/package/@ic-reactor/react)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
pnpm add @ic-reactor/react @icp-sdk/core @tanstack/react-query
|
|
16
|
-
|
|
17
|
-
# Optional: Internet Identity login helpers
|
|
18
|
-
pnpm add @icp-sdk/auth@^10
|
|
19
|
-
```
|
|
20
|
-
|
|
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.
|
|
10
|
+
The React bindings of ic-reactor 4: three `'use client'` exports over a client
|
|
11
|
+
made by [`@ic-reactor/core`](../core/README.md).
|
|
29
12
|
|
|
30
|
-
|
|
31
|
-
|
|
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`. |
|
|
32
18
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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`.
|
|
36
24
|
|
|
37
|
-
|
|
38
|
-
{ "overrides": { "@icp-sdk/auth": { "@icp-sdk/core": "$@icp-sdk/core" } } }
|
|
39
|
-
```
|
|
25
|
+
## Install
|
|
40
26
|
|
|
41
|
-
|
|
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.
|
|
85
|
-
>
|
|
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.
|
|
92
|
-
|
|
93
|
-
`@icp-sdk/auth` is an optional peer. `AuthenticationManager` reaches it through a
|
|
94
|
-
literal `import("@icp-sdk/auth/client")`, so Vite, Rollup and webpack code-split
|
|
95
|
-
it into its own async chunk. That chunk is never fetched unless something
|
|
96
|
-
touches authentication, and its bytes are dropped from the output entirely in
|
|
97
|
-
apps that never reference the class.
|
|
98
|
-
|
|
99
|
-
Bundlers still **resolve** that specifier while building the module graph, which
|
|
100
|
-
happens before any tree-shaking — so a missing peer cannot simply be optimized
|
|
101
|
-
away. The import therefore sits inside a `try` block, which webpack-family
|
|
102
|
-
bundlers treat as declaring an optional dependency: with the peer absent the
|
|
103
|
-
build succeeds and prints one warning,
|
|
104
|
-
`Module not found: Can't resolve '@icp-sdk/auth/client'`. Only the login paths
|
|
105
|
-
are affected, and they throw an actionable error if they are ever called.
|
|
106
|
-
|
|
107
|
-
Install the peer to remove the warning, or silence it with
|
|
108
|
-
[`ignoreWarnings`](https://webpack.js.org/configuration/other-options/#ignorewarnings):
|
|
109
|
-
|
|
110
|
-
```js
|
|
111
|
-
// webpack.config.js / next.config.js (webpack)
|
|
112
|
-
ignoreWarnings: [{ module: /@ic-reactor\/react/, message: /@icp-sdk\/auth/ }]
|
|
27
|
+
```bash
|
|
28
|
+
npm install @ic-reactor/core@beta @ic-reactor/react@beta @tanstack/react-query
|
|
113
29
|
```
|
|
114
30
|
|
|
115
|
-
|
|
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).
|
|
116
33
|
|
|
117
|
-
|
|
118
|
-
// src/reactor.ts
|
|
119
|
-
import { ClientManager, Reactor, createActorHooks } from "@ic-reactor/react"
|
|
120
|
-
import { QueryClient } from "@tanstack/react-query"
|
|
121
|
-
import { canisterId, idlFactory, type _SERVICE } from "./declarations/backend"
|
|
34
|
+
## Provide a client
|
|
122
35
|
|
|
123
|
-
|
|
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:
|
|
124
39
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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.
|
|
128
48
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
idlFactory,
|
|
132
|
-
name: "backend",
|
|
133
|
-
// `dfx` writes this alongside the idlFactory. Required — omit it only when
|
|
134
|
-
// the vite-plugin injects an `ic_env` cookie for this canister; outside that
|
|
135
|
-
// flow the constructor throws.
|
|
136
|
-
canisterId,
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
export const {
|
|
140
|
-
useActorQuery,
|
|
141
|
-
useActorMutation,
|
|
142
|
-
useActorSuspenseQuery,
|
|
143
|
-
useActorMethod,
|
|
144
|
-
} = createActorHooks(backend)
|
|
145
|
-
```
|
|
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).
|
|
146
51
|
|
|
147
52
|
```tsx
|
|
148
|
-
|
|
149
|
-
import { QueryClientProvider } from "@tanstack/react-query"
|
|
150
|
-
import { queryClient, useActorMethod, useActorQuery } from "./reactor"
|
|
151
|
-
|
|
152
|
-
function Greeting() {
|
|
153
|
-
const { data, isPending } = useActorQuery({
|
|
154
|
-
functionName: "greet",
|
|
155
|
-
args: ["World"],
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
if (isPending) return <p>Loading...</p>
|
|
159
|
-
return <p>{data}</p>
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function Increment() {
|
|
163
|
-
const { call, isPending } = useActorMethod({ functionName: "increment" })
|
|
53
|
+
"use client"
|
|
164
54
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
)
|
|
170
|
-
}
|
|
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"
|
|
171
59
|
|
|
172
|
-
export function
|
|
60
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
173
61
|
return (
|
|
174
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
62
|
+
<ReactorProvider
|
|
63
|
+
client={() =>
|
|
64
|
+
createClient({ network: "ic", auth: () => new AuthClient() })
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
{children}
|
|
68
|
+
</ReactorProvider>
|
|
178
69
|
)
|
|
179
70
|
}
|
|
180
71
|
```
|
|
181
72
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
191
|
-
- `createActorHooks(reactor)` for per-canister hooks like `useActorQuery` and
|
|
192
|
-
`useActorMutation`
|
|
193
|
-
- `createAuthHooks(authentication)` for `useAuth`, `useAgentState`, and
|
|
194
|
-
`useUserPrincipal`
|
|
195
|
-
- `createIdentityAttributeHooks(identityAttributes)` for signed identity
|
|
196
|
-
attribute requests
|
|
197
|
-
- direct reactor hooks like `useReactorQuery` when you want to pass the reactor
|
|
198
|
-
instance at call time
|
|
199
|
-
- factory helpers like `createQuery`, `createSuspenseQuery`,
|
|
200
|
-
`createInfiniteQuery`, `createSuspenseInfiniteQuery`, and `createMutation`
|
|
201
|
-
when the same operation must work both inside and outside React
|
|
202
|
-
|
|
203
|
-
## Choosing the Right Pattern
|
|
204
|
-
|
|
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.
|
|
209
|
-
- Use query and mutation factories when you also need loader, action, service,
|
|
210
|
-
or test usage through `.fetch()`, `.prefetch()`, `.execute()`, `.invalidate()`,
|
|
211
|
-
`.getCacheData()`, or `.setData()`.
|
|
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).
|
|
215
|
-
- Use generated hooks from `@ic-reactor/vite-plugin` or `@ic-reactor/cli` when
|
|
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.
|
|
263
|
-
|
|
264
|
-
## Factory Example
|
|
265
|
-
|
|
266
|
-
```ts
|
|
267
|
-
import { createSuspenseQueryFactory, createMutation } from "@ic-reactor/react"
|
|
268
|
-
import { backend } from "./reactor"
|
|
269
|
-
|
|
270
|
-
export const getProfile = createSuspenseQueryFactory(backend, {
|
|
271
|
-
functionName: "get_profile",
|
|
272
|
-
})
|
|
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.
|
|
273
77
|
|
|
274
|
-
|
|
275
|
-
functionName: "update_profile",
|
|
276
|
-
// Every get_profile query this factory made, whatever the args
|
|
277
|
-
invalidateQueries: [getProfile],
|
|
278
|
-
onCanisterError: (err) => console.error("Canister Err variant:", err.code),
|
|
279
|
-
})
|
|
280
|
-
```
|
|
78
|
+
A client the app owns, in a browser-only app:
|
|
281
79
|
|
|
282
80
|
```tsx
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
// React component
|
|
286
|
-
const { data } = profileQuery.useSuspenseQuery()
|
|
287
|
-
|
|
288
|
-
// Prefetch before navigating (fire-and-forget)
|
|
289
|
-
void profileQuery.prefetch()
|
|
290
|
-
|
|
291
|
-
// Write into the cache
|
|
292
|
-
profileQuery.setData({ id: "alice", name: "Alice" })
|
|
293
|
-
|
|
294
|
-
// Mutation with extra invalidation: a query object, a query factory, a
|
|
295
|
-
// `{ functionName, args? }` method of the reactor, or a query key
|
|
296
|
-
const mutation = updateProfile.useMutation({
|
|
297
|
-
invalidateQueries: [{ functionName: "list_profiles" }],
|
|
298
|
-
})
|
|
299
|
-
```
|
|
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
|
-
|
|
308
|
-
## Internet Identity
|
|
309
|
-
|
|
310
|
-
`defineReactor` wires up Internet Identity for you — `useAuth`,
|
|
311
|
-
`useUserPrincipal`, `useAgentState` and `useIdentityAttributes` come back
|
|
312
|
-
alongside the actor hooks:
|
|
313
|
-
|
|
314
|
-
```tsx
|
|
315
|
-
// src/reactor.ts
|
|
316
|
-
import { defineReactor } from "@ic-reactor/react"
|
|
317
|
-
import { canisterId, idlFactory, type _SERVICE } from "./declarations/backend"
|
|
318
|
-
|
|
319
|
-
export const { useActorQuery, useAuth, useIdentityAttributes, authentication } =
|
|
320
|
-
defineReactor<_SERVICE>({
|
|
321
|
-
name: "backend",
|
|
322
|
-
idlFactory,
|
|
323
|
-
canisterId,
|
|
324
|
-
auth: {
|
|
325
|
-
// Required when the app is served from more than one origin, so every
|
|
326
|
-
// origin resolves to the same principal.
|
|
327
|
-
derivationOrigin: "https://app.example.com",
|
|
328
|
-
},
|
|
329
|
-
})
|
|
330
|
-
```
|
|
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
|
-
|
|
339
|
-
Auth options are forwarded to the underlying `@icp-sdk/auth` client:
|
|
340
|
-
`identityProvider`, `derivationOrigin`, `windowOpenerFeatures`,
|
|
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
|
|
346
|
-
for `openIdProvider`; any other value is dropped, since raw issuer URLs are
|
|
347
|
-
only meaningful on `requestOpenIdAttributes`, where they scope the keys.
|
|
348
|
-
|
|
349
|
-
Pass `authentication` from one reactor into another to share a single session
|
|
350
|
-
across canisters. That reactor adopts the manager's `ClientManager` so sign-in
|
|
351
|
-
updates the agent it calls through, so pass either `authentication` or `auth` —
|
|
352
|
-
not both.
|
|
353
|
-
|
|
354
|
-
Set up manually when you need more control:
|
|
355
|
-
|
|
356
|
-
```tsx
|
|
357
|
-
// src/auth.ts
|
|
358
|
-
import {
|
|
359
|
-
AuthenticationManager,
|
|
360
|
-
IdentityAttributesManager,
|
|
361
|
-
createAuthHooks,
|
|
362
|
-
createIdentityAttributeHooks,
|
|
363
|
-
} from "@ic-reactor/react"
|
|
364
|
-
import { clientManager } from "./reactor"
|
|
365
|
-
|
|
366
|
-
const authentication = new AuthenticationManager({ clientManager })
|
|
367
|
-
export const { useAuth, useAgentState, useUserPrincipal } =
|
|
368
|
-
createAuthHooks(authentication)
|
|
369
|
-
|
|
370
|
-
const identityAttributes = new IdentityAttributesManager(authentication)
|
|
371
|
-
export const { useIdentityAttributes } =
|
|
372
|
-
createIdentityAttributeHooks(identityAttributes)
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
`useAuth()` calls `authentication.prepareClient()` on mount. Outside React, do
|
|
376
|
-
it yourself during startup — it preloads the auth module so `login()` can open
|
|
377
|
-
the identity provider window synchronously inside a click handler, which is
|
|
378
|
-
what browser popup blockers require:
|
|
379
|
-
|
|
380
|
-
```ts
|
|
381
|
-
// once, at startup — awaits the dynamic import and builds the AuthClient
|
|
382
|
-
await authentication.prepareClient()
|
|
383
|
-
|
|
384
|
-
// later, inside the click handler — no await before signIn(), so the popup
|
|
385
|
-
// still counts as user-initiated
|
|
386
|
-
button.onclick = () => authentication.login()
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
`authentication.getPreparedClient()` returns the already-built client
|
|
390
|
-
synchronously, or `undefined` when the module has not loaded yet.
|
|
391
|
-
|
|
392
|
-
If your bundler cannot resolve the optional peer at all, construct the client
|
|
393
|
-
yourself and inject it — IC Reactor then never imports `@icp-sdk/auth`:
|
|
81
|
+
"use client"
|
|
394
82
|
|
|
395
|
-
|
|
396
|
-
import {
|
|
83
|
+
import { createClient } from "@ic-reactor/core"
|
|
84
|
+
import { ReactorProvider } from "@ic-reactor/react"
|
|
397
85
|
import { AuthClient } from "@icp-sdk/auth/client"
|
|
86
|
+
import type { ReactNode } from "react"
|
|
398
87
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
### Local Internet Identity
|
|
406
|
-
|
|
407
|
-
On a local replica the provider URL is resolved for you. `prepareClient()`
|
|
408
|
-
queries the local Internet Identity canister's `http_request` to see what the
|
|
409
|
-
installed build actually serves, then targets `/authorize` (release-2026-01-05 …
|
|
410
|
-
release-2026-03-16) or the legacy `/#authorize` (up to release-2025-03-07, which
|
|
411
|
-
also logs a console warning). If the build serves neither, `login()` throws an
|
|
412
|
-
actionable error rather than opening a popup onto the gateway's
|
|
413
|
-
verification-error page. From `release-2026-03-23` the II frontend moved out of
|
|
414
|
-
the canister, so no local build past that point can be used for sign-in — pin an
|
|
415
|
-
older release in `dfx.json`.
|
|
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
|
-
|
|
423
|
-
An inconclusive probe — the canister unreachable, or answering no `http_request`
|
|
424
|
-
— does not throw: it keeps `/authorize`, because a diagnostic that blocks a
|
|
425
|
-
login that might have worked is worse than the failure it explains. To override
|
|
426
|
-
the URL yourself, `localInternetIdentityProvider(port, canisterId?, authorizePath?)`
|
|
427
|
-
takes the path as its third argument.
|
|
428
|
-
|
|
429
|
-
## Identity Attributes / OpenID email and profile values
|
|
430
|
-
|
|
431
|
-
Identity attributes use a dedicated `IdentityAttributesManager`, with React
|
|
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
|
|
435
|
-
either a value or a callback and adapts it, but the callback form is what
|
|
436
|
-
preserves the user gesture (see below).
|
|
437
|
-
|
|
438
|
-
**Pass the nonce as a callback.** Awaiting your backend before calling
|
|
439
|
-
`requestOpenIdAttributes` ends the user gesture, and the browser then blocks
|
|
440
|
-
the Internet Identity window:
|
|
441
|
-
|
|
442
|
-
```tsx
|
|
443
|
-
// ✅ window opens immediately, nonce resolves while the user is in II
|
|
444
|
-
await requestOpenIdAttributes({
|
|
445
|
-
nonce: () => backend.callMethod({ functionName: "register_begin" }),
|
|
446
|
-
openIdProvider: "google",
|
|
447
|
-
keys: ["email", "name"],
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
// ❌ gesture is gone by the time the window would open
|
|
451
|
-
const nonce = await backend.callMethod({ functionName: "register_begin" })
|
|
452
|
-
await requestOpenIdAttributes({
|
|
453
|
-
nonce,
|
|
454
|
-
openIdProvider: "google",
|
|
455
|
-
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(),
|
|
456
92
|
})
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
```tsx
|
|
460
|
-
// src/RegisterWithOpenIdProvider.tsx
|
|
461
|
-
import { useIdentityAttributes } from "./auth"
|
|
462
|
-
import { backend } from "./reactor"
|
|
463
|
-
|
|
464
|
-
function RegisterWithOpenIdProvider() {
|
|
465
|
-
const {
|
|
466
|
-
requestOpenIdAttributes,
|
|
467
|
-
attributes,
|
|
468
|
-
isRequestingAttributes,
|
|
469
|
-
attributeError,
|
|
470
|
-
} = useIdentityAttributes()
|
|
471
|
-
|
|
472
|
-
async function handleProviderLogin() {
|
|
473
|
-
const result = await requestOpenIdAttributes({
|
|
474
|
-
nonce: () => backend.callMethod({ functionName: "register_begin" }),
|
|
475
|
-
openIdProvider: "microsoft",
|
|
476
|
-
keys: ["email", "name"],
|
|
477
|
-
// Optional: any window.open() features string, e.g. to size the popup
|
|
478
|
-
windowOpenerFeatures: "width=500,height=640",
|
|
479
|
-
})
|
|
480
|
-
|
|
481
|
-
console.log(result.decodedAttributes.email)
|
|
482
|
-
console.log(result.decodedAttributes.name)
|
|
483
|
-
|
|
484
|
-
await backend.callMethod({
|
|
485
|
-
functionName: "register_finish",
|
|
486
|
-
args: [
|
|
487
|
-
{
|
|
488
|
-
data: result.signedAttributes.data,
|
|
489
|
-
signature: result.signedAttributes.signature,
|
|
490
|
-
},
|
|
491
|
-
],
|
|
492
|
-
})
|
|
493
|
-
}
|
|
494
93
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
{attributes?.decodedAttributes.email ??
|
|
498
|
-
attributeError?.message ??
|
|
499
|
-
"Continue with provider"}
|
|
500
|
-
</button>
|
|
501
|
-
)
|
|
94
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
95
|
+
return <ReactorProvider client={() => client}>{children}</ReactorProvider>
|
|
502
96
|
}
|
|
503
97
|
```
|
|
504
98
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
`my_profile`) is handed to whichever request asks next:
|
|
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
|
|
522
115
|
|
|
523
116
|
```tsx
|
|
524
|
-
|
|
525
|
-
import { defineReactor } from "@ic-reactor/react"
|
|
117
|
+
import { useAuth } from "@ic-reactor/react"
|
|
526
118
|
|
|
527
|
-
export
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
541
|
-
export default async function Page() {
|
|
542
|
-
const clientManager = new ClientManager({ queryClient: new QueryClient() })
|
|
543
|
-
const reactor = new Reactor<_SERVICE>({
|
|
544
|
-
name: "backend",
|
|
545
|
-
clientManager,
|
|
546
|
-
idlFactory,
|
|
547
|
-
canisterId,
|
|
548
|
-
})
|
|
549
|
-
|
|
550
|
-
const data = await reactor.fetchQuery({ functionName: "get_my_profile" })
|
|
551
|
-
return <Profile data={data} />
|
|
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}
|
|
124
|
+
</button>
|
|
125
|
+
) : (
|
|
126
|
+
<button onClick={() => signIn().catch(console.error)}>Sign in</button>
|
|
127
|
+
)
|
|
552
128
|
}
|
|
553
129
|
```
|
|
554
130
|
|
|
555
|
-
|
|
556
|
-
`
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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.
|
|
561
153
|
|
|
562
154
|
```tsx
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
import {
|
|
566
|
-
|
|
567
|
-
export
|
|
568
|
-
|
|
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>
|
|
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>
|
|
580
164
|
}
|
|
581
165
|
```
|
|
582
166
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
the
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
a
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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.
|
|
709
|
-
|
|
710
|
-
## Canister Error Handling
|
|
711
|
-
|
|
712
|
-
Canister methods can return `Result { Err: E }` variants. These are surfaced
|
|
713
|
-
as `CanisterError` and can be handled separately from network or agent errors
|
|
714
|
-
via `onCanisterError`. This callback is supported on both `createMutation` and
|
|
715
|
-
the direct `useActorMutation` hook:
|
|
716
|
-
|
|
717
|
-
```tsx
|
|
718
|
-
import { createMutation } from "@ic-reactor/react"
|
|
719
|
-
import { backend, useActorMutation } from "./reactor"
|
|
720
|
-
|
|
721
|
-
// Via createActorHooks
|
|
722
|
-
const { mutate } = useActorMutation({
|
|
723
|
-
functionName: "transfer",
|
|
724
|
-
onCanisterError: (err, vars) => {
|
|
725
|
-
// err.code — the Err variant key (e.g. "InsufficientFunds")
|
|
726
|
-
// err.err — the typed Err value
|
|
727
|
-
console.error(`Transfer failed: ${err.code}`, vars)
|
|
728
|
-
},
|
|
729
|
-
onError: (err) => {
|
|
730
|
-
// Fires for ALL errors: canister Err variants, network failures, etc.
|
|
731
|
-
console.error("Unexpected error", err)
|
|
732
|
-
},
|
|
733
|
-
})
|
|
734
|
-
|
|
735
|
-
// Via createMutation factory
|
|
736
|
-
const transferMutation = createMutation(backend, {
|
|
737
|
-
functionName: "transfer",
|
|
738
|
-
onCanisterError: (err) => toast.error(`${err.code}`),
|
|
739
|
-
})
|
|
740
|
-
```
|
|
741
|
-
|
|
742
|
-
## Re-exports
|
|
743
|
-
|
|
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):
|
|
748
|
-
|
|
749
|
-
- `ClientManager`
|
|
750
|
-
- `Reactor`
|
|
751
|
-
- `DisplayReactor`
|
|
752
|
-
- `CallError`
|
|
753
|
-
- `CanisterError`
|
|
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.
|
|
826
|
-
|
|
827
|
-
## See Also
|
|
828
|
-
|
|
829
|
-
- Docs: https://ic-reactor.b3pay.net/v3/packages/react
|
|
830
|
-
- `@ic-reactor/core`: ../core/README.md
|
|
831
|
-
- `@ic-reactor/vite-plugin`: ../vite-plugin/README.md
|
|
832
|
-
- `@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 global
|
|
229
|
+
error listener, an effect that awaits the fetch and, for a
|
|
230
|
+
`useSuspenseQuery` read the server rendered without dehydrating its data,
|
|
231
|
+
React's `onRecoverableError` (as the reported error's `cause` on React 19)
|
|
232
|
+
see it: ignore `kind` `"cancelled"` there. The client owns its
|
|
233
|
+
`QueryClient` and takes no `QueryCache`, so a global listener is
|
|
234
|
+
`client.queryClient.getQueryCache().subscribe()`, which sees the error as
|
|
235
|
+
an `"updated"` event whose `action.type` is `"error"`.
|
|
236
|
+
- **A `useSuspenseQuery` read needs a Suspense boundary above it, on React 18
|
|
237
|
+
and 19 alike.** The move to the session is a synchronous update, and a
|
|
238
|
+
`useSuspenseQuery` read with none of the user's data yet suspends it. With a
|
|
239
|
+
boundary above the read, the boundary shows its fallback until the user's
|
|
240
|
+
data arrives, then the data, and the rest of the page responds meanwhile.
|
|
241
|
+
With none, React 18 refuses the update ("A component suspended while
|
|
242
|
+
responding to synchronous input") and unmounts the root: a signed-in reload
|
|
243
|
+
renders nothing. React 19 keeps the server's HTML on screen, but until the
|
|
244
|
+
user's data arrives, however long the read and its retries take, a click or
|
|
245
|
+
any other update outside a transition commits nothing, and neither does a
|
|
246
|
+
transition that renders the reading component again. React expects a
|
|
247
|
+
boundary above any component that suspends anyway: put one there.
|
|
248
|
+
|
|
249
|
+
## Life of the client
|
|
250
|
+
|
|
251
|
+
The provider disposes a client its factory created when it unmounts: the
|
|
252
|
+
client stops listening to the auth, disposes it, and clears the `QueryClient`.
|
|
253
|
+
React's development double-mount (`StrictMode` unmounts and mounts every
|
|
254
|
+
component at once) does not dispose a client in use, because the disposal is
|
|
255
|
+
scheduled for the next macrotask and the second mount cancels it. A client the
|
|
256
|
+
app owns is never disposed by a provider.
|
|
257
|
+
|
|
258
|
+
React runs no cleanup for a render it throws away before committing it, such
|
|
259
|
+
as a provider's first render below a Suspense boundary that suspends. A client
|
|
260
|
+
the factory built for such a render, whose auth a `useAuth()` below may
|
|
261
|
+
already have built, is disposed once that render's state is garbage collected
|
|
262
|
+
(through a `FinalizationRegistry`, in a browser): later than an unmount would,
|
|
263
|
+
but its auth and listeners do not outlive it. A client the factory returns
|
|
264
|
+
again, or that a mounted provider runs on, is never disposed this way, whoever
|
|
265
|
+
created it.
|
|
266
|
+
|
|
267
|
+
Only the factory of the first render is used: passing another function on a
|
|
268
|
+
later render does not rebuild the client. To replace the client, give the
|
|
269
|
+
provider another `key`.
|
|
270
|
+
|
|
271
|
+
When React shows a hidden `Activity` again after the client the provider owns
|
|
272
|
+
was disposed, the provider builds another one with the same factory. That
|
|
273
|
+
client starts with an empty cache: hiding a provider inside an `Activity` drops
|
|
274
|
+
everything it had fetched, because React cannot tell a hidden subtree from an
|
|
275
|
+
unmounted one when it cleans up. To keep the cache across hiding, render the
|
|
276
|
+
provider above the `Activity`, or give it a client the app owns.
|
|
277
|
+
|
|
278
|
+
## 3.x
|
|
279
|
+
|
|
280
|
+
The released 3.x package, with its hook factories and auth hooks, is documented
|
|
281
|
+
at https://ic-reactor.b3pay.net/v3/packages/react. Its source and security fixes
|
|
282
|
+
live on the `main` branch.
|