Skip to content
IC Reactor

useClient

useClient(): Client

Defined in: react/src/index.tsx:528

The client of the nearest ReactorProvider: the one object that builds canister handles and query options, and that signs users in and out.

Call it in the body of each component that builds keys or options, on every render, and build them from what it returns there, never from a client held at module scope: they are built for the caller this render shows. Do not keep what it returns past the render: while a page hydrates it is a view for the anonymous caller, and a view never moves on. An effect or a callback that uses it closes over its render’s value and lists it in its dependencies (react-hooks/exhaustive-deps), so that it runs again with the client after hydrating. Kept from the hydrating render anywhere else (useState(client), useRef(client), a useMemo or useCallback with [], a module variable), it stays that view: the component shows the anonymous caller’s data, a read it starts while the user is signed in is cancelled, and a write through it still signs as the live caller. Nothing warns about it. A nested ReactorProvider given it (client={() => client}) holds the client the view was made over. It follows the client’s caller with useSyncExternalStore, so a component that calls it renders again when the caller changes (a sign-in, a switch of account, a sign-out) and never otherwise: a change of status that leaves the caller as it is (a session that expired, or one signed in elsewhere, both call as the anonymous principal) renders nothing. In the steady state it returns the provider’s client object itself.

On a server, and while a page hydrates, the caller is the anonymous one, as for useAuth. In a browser that holds a session, the hydrating render gets a view of the client for the anonymous caller: its queryKey, queryOptions, caller() and authState() are the anonymous caller’s, so the page finds what the server prefetched and dehydrated and matches its HTML; everything else (canisters, mutationOptions, signIn, signOut, the QueryClient) is the client’s own, and a write signs as the caller current when it runs. Right after hydrating, React renders the component again with the client itself, for the user: a read with none of the user’s data yet shows its loading state, and a useSuspenseQuery read its boundary’s fallback, until that data arrives. A client built with identity keeps its caller for good and is always returned as it is.

Three consequences of that move on a signed-in reload:

  • A Suspense boundary that is still dehydrated below a component that renders with the caller (this hook or useAuth), because its lazy code is still loading or its streamed HTML has not arrived, is rendered on the client when that component moves on: it shows its fallback instead of the server’s HTML, and React 18 reports a recoverable error. Its data is still the user’s. A useAuth() component also moves on in a tab whose session expired or is signed in elsewhere. Render such a boundary where no component that renders with the caller sits above it, or pass it in as children, which a component’s own update does not render again.
  • A read the hydrating render built may still run once (TanStack Query refetches stale data on mount, and an effect may fetch with the view’s options). It is cancelled before anything is sent. A key that holds data keeps it as it was, and a fetch of it resolves with that data. A key with none fails (kind "cancelled", code "caller_changed") until the anonymous caller is current again: the hydrating render’s own reads never show that, but a listener of client.queryClient.getQueryCache().subscribe() (the client takes no QueryCache of yours), an effect that awaits the fetch and, for a useSuspenseQuery read the server rendered without dehydrating its data, React’s onRecoverableError (as the reported error’s cause on React 19) see it, so ignore kind "cancelled" there.
  • Put a Suspense boundary above every component that reads with useSuspenseQuery, on React 18 and 19 alike. The move is a synchronous update, which such a read suspends until the user’s data arrives. With a boundary above, the boundary shows its fallback, then the user’s data, and the rest of the page responds meanwhile. With none, React 18 refuses the update (“A component suspended while responding to synchronous input”) and unmounts the root, so a signed-in reload renders nothing. React 19 keeps the server’s HTML on screen, but until the user’s data arrives, however long the read and its retries take, a click or any other update outside a transition commits nothing, and neither does a transition that renders the reading component again.

Client

Error outside a ReactorProvider, naming it.