React Setup
This guide covers the complete setup of IC Reactor in a React application, including authentication with Internet Identity.
Basic Setup
Section titled “Basic Setup”-
Create the Reactor
Section titled “Create the Reactor”Set up your central configuration:
src/reactor/index.ts import { ClientManager, Reactor } from "@ic-reactor/react"import { QueryClient } from "@tanstack/react-query"import { idlFactory, type _SERVICE } from "../declarations/backend"export const queryClient = new QueryClient()export const clientManager = new ClientManager({ queryClient })export const backend = new Reactor<_SERVICE>({clientManager,idlFactory,name: "backend",canisterId: import.meta.env.VITE_BACKEND_CANISTER_ID,}) -
Create Hooks
Section titled “Create Hooks”src/reactor/hooks.ts import {AuthenticationManager,createActorHooks,createAuthHooks,} from "@ic-reactor/react"import { backend, clientManager } from "./index"// Actor hooks for your canisterexport const {useActorQuery,useActorMutation,useActorSuspenseQuery,useActorInfiniteQuery,} = createActorHooks(backend)// Auth hooks are bound to an AuthenticationManager, not the ClientManagerexport const authentication = new AuthenticationManager({ clientManager })export const {useAuth,useUserPrincipal,useAgentState,} = createAuthHooks(authentication) -
Setup Provider (Optional)
Section titled “Setup Provider (Optional)”While not required (as hooks are already bound to your reactor and its internal
queryClient), you can wrap your application withQueryClientProviderto use additional TanStack Query features or to enable the DevTools:src/App.tsx import { QueryClientProvider } from "@tanstack/react-query"import { ReactQueryDevtools } from "@tanstack/react-query-devtools"import { queryClient } from "./reactor"import Router from "./Router"function App() {return (<QueryClientProvider client={queryClient}><Router /><ReactQueryDevtools initialIsOpen={false} /></QueryClientProvider>)}export default App
Authentication
Section titled “Authentication”useAuth Hook
Section titled “useAuth Hook”The useAuth hook provides login and logout functionality:
import { useAuth } from "../reactor/hooks"
function LoginButton() { const { login, logout, isAuthenticated, isAuthenticating } = useAuth()
if (isAuthenticating) { return <button disabled>Connecting...</button> }
return isAuthenticated ? ( <button onClick={logout}>Logout</button> ) : ( <button onClick={() => login()}>Login with Internet Identity</button> )}useAgentState Hook
Section titled “useAgentState Hook”For accessing the useAgentState connection state:
import { useAgentState } from "../reactor/hooks"
function ConnectionStatus() { const { isInitialized, // boolean isInitializing, // boolean network, // 'ic' | 'local' | 'remote' } = useAgentState()
if (isInitializing) { return <span>Connecting to network...</span> }
return <span>Connected to {network}</span>}useUserPrincipal Hook
Section titled “useUserPrincipal Hook”A convenience hook for getting the current user’s principal. See useUserPrincipal reference:
import { useUserPrincipal } from "../reactor/hooks"
function UserBadge() { const principal = useUserPrincipal()
if (!principal) return null
return <div className="badge">{principal.toText().slice(0, 8)}...</div>}Complete Auth Component
Section titled “Complete Auth Component”import { useAuth, useUserPrincipal } from "../reactor/hooks"
function AuthSection() { const { login, logout, isAuthenticated, isAuthenticating } = useAuth() const principal = useUserPrincipal()
if (isAuthenticating) { return <div className="auth">Connecting to Internet Identity...</div> }
return ( <div className="auth"> {isAuthenticated ? ( <> <span>Welcome, {principal?.toText().slice(0, 12)}...</span> <button onClick={logout}>Logout</button> </> ) : ( <button onClick={() => login()}>Login with Internet Identity</button> )} </div> )}Login Options
Section titled “Login Options”Customize the login flow:
const { login } = useAuth()
// With optionslogin({ identityProvider: "https://id.ai/authorize", maxTimeToLive: BigInt(7 * 24 * 60 * 60 * 1_000_000_000), // 7 days onSuccess: () => { console.log("Logged in successfully!") navigate("/dashboard") }, onError: (error) => { console.error("Login failed:", error) },})Protected Routes
Section titled “Protected Routes”Create a wrapper for protected routes:
import { useAuth } from "../reactor/hooks"import { Navigate, useLocation } from "react-router-dom"
function ProtectedRoute({ children }: { children: React.ReactNode }) { const { isAuthenticated, isAuthenticating } = useAuth() const location = useLocation()
if (isAuthenticating) { return <LoadingSpinner /> }
if (!isAuthenticated) { return <Navigate to="/login" state={{ from: location }} replace /> }
return children}
// Usage in router;<Route path="/dashboard" element={ <ProtectedRoute> <Dashboard /> </ProtectedRoute> }/>Multiple Actors
Section titled “Multiple Actors”Working with multiple canisters:
import { ClientManager, Reactor } from "@ic-reactor/react"import { QueryClient } from "@tanstack/react-query"import { idlFactory as ledgerIdl, type _SERVICE as Ledger,} from "../declarations/ledger"import { idlFactory as backendIdl, type _SERVICE as Backend,} from "../declarations/backend"
export const queryClient = new QueryClient()export const clientManager = new ClientManager({ queryClient })
// Ledger canisterexport const ledger = new Reactor<Ledger>({ clientManager, name: "ledger", idlFactory: ledgerIdl, canisterId: import.meta.env.VITE_LEDGER_CANISTER_ID,})
// Backend canisterexport const backend = new Reactor<Backend>({ clientManager, name: "backend", idlFactory: backendIdl, canisterId: import.meta.env.VITE_BACKEND_CANISTER_ID,})import { AuthenticationManager, createActorHooks, createAuthHooks,} from "@ic-reactor/react"import { ledger, backend, clientManager } from "./index"
export const authentication = new AuthenticationManager({ clientManager })
export const ledgerHooks = createActorHooks(ledger)export const backendHooks = createActorHooks(backend)export const authHooks = createAuthHooks(authentication)
// Export individual hooks for convenienceexport const { useActorQuery: useLedgerQuery, useActorMutation: useLedgerMutation,} = ledgerHooks
export const { useActorQuery: useBackendQuery, useActorMutation: useBackendMutation,} = backendHooks
export const { useAuth, useUserPrincipal } = authHooksAuto Transformations
Section titled “Auto Transformations”For easier handling of Candid types, use DisplayReactor:
import { DisplayReactor } from "@ic-reactor/react"import { idlFactory, type _SERVICE } from "../declarations/backend"
const backend = new DisplayReactor<_SERVICE>({ clientManager, idlFactory, name: "backend", canisterId: "...",})
// Now BigInt values are returned as strings,// Principals as text, etc.Session Persistence
Section titled “Session Persistence”Sessions are automatically persisted in IndexedDB. Authentication hooks like useAuth automatically initialize the agent and restore the session on first use.
To restore the session yourself on app load (optional), call
authentication.authenticate() — clientManager.initialize() only initializes
the agent:
import { useEffect } from "react"import { authentication } from "./reactor/hooks"
function App() { useEffect(() => { authentication.authenticate() }, [])
return <YourApp />}Or check agent state:
import { useAgentState } from "./reactor/hooks"
function App() { const { isInitialized, isInitializing, error } = useAgentState()
if (isInitializing) { return <LoadingScreen /> }
if (error) { return <ErrorScreen error={error} /> }
return <YourApp />}Server-Side Rendering
Section titled “Server-Side Rendering”Build the reactor inside the request, not at module scope.
Every setup on this page defines the reactor at module scope, which is exactly
right for a client-only SPA — but on a server it is a data leak. A reactor owns
its QueryClient, and module scope outlives the request: every request served
by the same server module graph shares one reactor and one cache. No segment
of a reactor query key identifies the caller, so a cached result for a
caller-scoped method (get_my_balance, a deposit address, my_profile) is
handed to whichever request asks next:
import { defineReactor } from "@ic-reactor/react"import { QueryClient } from "@tanstack/react-query"import { canisterId, idlFactory, type _SERVICE } from "../declarations/backend"
// ❌ Shared by every request on the serverexport const app = defineReactor<_SERVICE>({ name: "backend", idlFactory, canisterId,})// ✅ Per request: nothing is shared between usersexport default async function Page() { const app = defineReactor<_SERVICE>({ name: "backend", idlFactory, canisterId, queryClient: new QueryClient(), })
const data = await app.reactor.fetchQuery({ functionName: "get_my_profile" }) return <Profile data={data} />}The fresh reactor’s agent is anonymous. A per-request QueryClient
isolates the cache; it does not carry the caller’s identity. For a
caller-scoped method like get_my_profile to answer as the request’s user,
thread that request’s identity into the reactor — agentOptions: { identity }
on the defineReactor call — however your server derives it.
Two further constraints on the Next.js App Router specifically:
- Hooks are client-only, like every React hook — call them from a
"use client"module. A server component may importReactor/ClientManagerand make imperative calls; that path works. - Hooks bind to their reactor’s own
QueryClientrather than to aQueryClientProvider, soHydrationBoundaryprefetch does not feed them unless the provider’s client is that reactor’s client. Next.js also evaluates a shared module twice on the server (the RSC and SSR graphs), so a module-scope reactor is two different instances there.
If none of that applies — a client-only SPA — module-scope reactors are exactly right and none of this is a concern.
Best Practices
Section titled “Best Practices”- Centralize configuration — Keep reactor setup in a dedicated
reactor/folder - Use environment variables — Store canister IDs in
.envfiles - Enable DevTools — Use React Query DevTools in development
- Handle auth state — Always check
isAuthenticatingbefore showing login UI - Name your actors — Use the
nameoption for better debugging - Use Suspense boundaries — Wrap components using
useActorSuspenseQueryin<Suspense>