@ic-reactor/react
@ic-reactor/react is the React binding of ic-reactor 4: three
"use client" exports over a client made by
@ic-reactor/core. There is no hook that wraps
useQuery or useMutation. You read and write canisters with TanStack Query’s
own hooks and the options the client builds, client.queryOptions(...) and
client.mutationOptions(...).
The package never re-exports @ic-reactor/core, so every name has one import
path.
Like core, it ships as ES modules only (see ES modules only).
Install
Section titled “Install”npm install @ic-reactor/core @ic-reactor/react @tanstack/react-querynpm install @icp-sdk/core @tanstack/query-corenpm install --save-exact @candid-core/schema@0.4.0npm install @icp-sdk/auth # for Internet IdentityThe second and third lines are peers of @ic-reactor/core;
see its package page. For a Vite app, see also
the Vite plugin.
Peers of this package:
| Peer | Range |
|---|---|
@ic-reactor/core |
exactly this package’s version: the two are released together |
@tanstack/react-query |
^5.90.2 |
react |
>=18.0.0 |
Node >=22.12.0, the first Node 22 that loads an ES module with require()
(22.12 itself prints an experimental warning when it does).
One copy of @tanstack/query-core
Section titled “One copy of @tanstack/query-core”Install @tanstack/query-core at the exact version your
@tanstack/react-query depends on (npm view @tanstack/react-query@<version> dependencies), and check that npm ls @tanstack/query-core shows one copy.
npm installs a second copy when the two differ: core then builds its
QueryClient from one copy and the hooks read the other, each with its own
skipToken. client.queryOptions() throws a TypeError for the hooks’
skipToken, and in development ReactorProvider throws when it is handed
such a client. Both errors name this fix.
Exports
Section titled “Exports”The entry has four names. The export budget allows five.
| Export | What it is |
|---|---|
ReactorProvider |
Gives a tree one client, and TanStack Query that client’s QueryClient. |
useClient |
The client of the nearest provider, for the caller this render shows. |
useAuth |
Who calls (status, principal), with signIn and signOut. |
ReactorProviderProps |
A type: { client: () => Client; children: ReactNode }. |
Nothing else is importable: the only subpaths are . and ./package.json.
useQuery, useMutation, useSuspenseQuery, skipToken, dehydrate and
HydrationBoundary come from @tanstack/react-query, and principal from
@candid-core/schema.
ReactorProvider
Section titled “ReactorProvider”interface ReactorProviderProps { readonly client: () => Client readonly children: ReactNode}ReactorProvider takes a factory, not a client, and calls it once for each
mounted provider. It renders TanStack’s QueryClientProvider with
client.queryClient. When it unmounts, it disposes the client only if that
factory call created it:
- The provider owns the client. A factory that creates it,
client={() => createClient({ ... })}, gives each mounted provider a client of its own, and the provider disposes it when it unmounts. Use this in an app that renders on a server: the factory runs once per request. - The app owns the client. A client created at module scope, one per tab
and used outside React too, is handed over as
client={() => client}. The provider borrows it and never disposes it.
"use client"
import { createClient } from "@ic-reactor/core"import { ReactorProvider } from "@ic-reactor/react"import { AuthClient } from "@icp-sdk/auth/client"import type { ReactNode } from "react"
export function Providers({ children }: { children: ReactNode }) { return ( <ReactorProvider client={() => createClient({ network: "ic", auth: (network) => new AuthClient(network), }) } > {children} </ReactorProvider> )}Who owns a client follows from when it was created, not from where the factory
is written. A getter such as client ??= createClient(...) makes the first
provider that calls it the owner. Create a shared client eagerly instead. A
Server Component cannot pass the factory across the client boundary, so in
Next.js the provider sits in a client module that the layout renders: see
SSR and hydration.
An app that already uses TanStack Query keeps one QueryClient per tree,
the client’s. ReactorProvider provides client.queryClient, so the app’s own
queries, REST calls included, run on it: an error that is not a ReactorError
is retried as TanStack Query retries it by default (up to 3 times in a
browser, never on a server), and client.dispose() clears those queries
along with the client’s reads. Set other defaults on each query. A
QueryClientProvider of the app’s own nested inside the provider works too: a
write invalidates its reads on the QueryClient it ran on as well as on the
client’s. That QueryClient lacks the client’s dehydrate and hydrate defaults,
so a server render dehydrates client.queryClient.
useClient
Section titled “useClient”useClient() returns the client of the nearest provider. It throws outside a
provider. Call it in the body of each component that builds keys or read
options, on every render:
import { useClient } from "@ic-reactor/react"import { useQuery } from "@tanstack/react-query"import { actor, type Actor } from "./generated/icrc1"
export function Fee({ id }: { id: string }) { const client = useClient() const ledger = client.canister<Actor>(actor, { id }) const fee = useQuery(client.queryOptions(ledger, "icrc1_fee")) return <span>{fee.data?.toString() ?? "…"}</span>}It follows the client’s caller, so a component that calls it renders again on a sign-in, a switch of account and a sign-out, and builds the new caller’s keys. A change of status that leaves the caller as it is renders nothing.
It returns a view of the client for the caller the render shows (the anonymous
one while a page hydrates): one object per caller, a new one when the caller
changes, the same one on every render while it stays. List it in the
dependencies of a useMemo, an effect or a callback built from it; React
Compiler memoizes on it the same way, so what is memoized is built again for
the new caller. Its mutationOptions send only as that caller: a write
pressed before a switch reached the render is refused (caller_changed) and
sends nothing. It is never the provider’s client object, so compare two
clients by their queryClient, and do not keep its result past its render.
See SSR and hydration.
useAuth
Section titled “useAuth”useAuth() returns { status, principal, expiresAt?, signIn, signOut }: the
client’s AuthState with sign-in and sign-out forwarded to the client. It
renders a component once for each change of status, principal or expiresAt,
and when a session ends by the clock. It is "anonymous" on
a server and during a hydrating render. See Auth.
Life of the client
Section titled “Life of the client”- StrictMode. The development double-mount does not dispose a client in use: disposal is scheduled for the next macrotask, and the second mount cancels it.
- A render React throws away. React runs no cleanup for a render it drops
before committing it, such as a provider’s first render below a Suspense
boundary that suspends. A client the factory built for it is disposed once
that render’s state is garbage collected (through a
FinalizationRegistry, in a browser). - Replacing the client. Only the factory of the first render is used.
Passing another function later does not rebuild the client. To replace the
client, give the provider another
key. - Activity. If React shows a hidden
Activityagain after the client the provider owns was disposed, the provider builds another one with the same factory, and it starts with an empty cache. To keep the cache across hiding, render the provider above theActivity, or give it a client the app owns.
Where to go next
Section titled “Where to go next”- Auth:
useAuth, sign-in and caller changes. - Reads and Writes: the options the client builds for TanStack Query.
- SSR and hydration: one client per request, and the limits of a signed-in reload.
- Testing: a provider with a borrowed test client.
- Examples: the Vite wallet and the Next.js app.
The guide for both packages is llms.txt, shipped in
node_modules/@ic-reactor/core/llms.txt; the React package’s own llms.txt
points to it. The released 3.x package is documented on
Migrating from 3.x.