Skip to content

@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).

Terminal window
npm install @ic-reactor/core @ic-reactor/react @tanstack/react-query
npm install @icp-sdk/core @tanstack/query-core
npm install --save-exact @candid-core/schema@0.4.0
npm install @icp-sdk/auth # for Internet Identity

The 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).

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.

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.

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() 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() 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.

  • 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 Activity again 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 the Activity, or give it a client the app owns.
  • 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.