HttpAgent
Manages the IC HTTP agent for all canister communication, handling network detection and initialization
ClientManager is the central orchestrator that unifies three essential concerns of IC development into a single, coherent system:
HttpAgent
Manages the IC HTTP agent for all canister communication, handling network detection and initialization
AuthClient
Coordinates authentication with Internet Identity, including login, logout, and session restoration
QueryClient
Integrates TanStack Query for automatic caching, deduplication, and background data updates
Without ClientManager, connecting to the IC typically involves:
HttpAgent manuallyAuthClient lifecycle separatelyClientManager solves this by providing a unified interface where all these concerns are managed together. When a user logs in, the agent automatically updates, all queries invalidate, and your reactors seamlessly use the new identity.
// One ClientManager, shared by all your reactorsconst clientManager = new ClientManager({ queryClient, withProcessEnv: true })
// Create multiple reactors - they all share the same agent and auth stateconst backend = new Reactor<BackendService>({ clientManager, idlFactory, canisterId,})const ledger = new Reactor<LedgerService>({ clientManager, idlFactory: ledgerIdl, canisterId: ledgerId,})
// Login once - all reactors automatically use the new identityawait clientManager.login()import { ClientManager } from "@ic-reactor/core"import { ClientManager } from "@ic-reactor/core"import { QueryClient } from "@tanstack/query-core"
const queryClient = new QueryClient()
const clientManager = new ClientManager({ queryClient, withProcessEnv: true,})| Option | Type | Default | Description |
|---|---|---|---|
queryClient | QueryClient | Required | TanStack Query client instance |
authClient | AuthClient | - | Pre-initialized AuthClient instance |
withProcessEnv | boolean | false | Auto-detect network from DFX_NETWORK |
withLocalEnv | boolean | false | Force local development mode |
port | number | 4943 | Local replica port |
agentOptions | HttpAgentOptions | {} | Additional HttpAgent options |
| Property | Type | Description |
|---|---|---|
agent | HttpAgent | The IC HTTP agent (getter) |
queryClient | QueryClient | TanStack Query client |
agentState | AgentState | Current agent initialization state |
authState | AuthState | Current authentication state |
agentHost | URL | The host URL of the agent |
network | "ic" | "local" | "remote" | Current network type |
isLocal | boolean | Whether connected to local replica |
interface AgentState { isInitialized: boolean isInitializing: boolean isLocalhost: boolean network: "ic" | "local" | "remote" | undefined error: Error | undefined}interface AuthState { identity: Identity | null isAuthenticated: boolean isAuthenticating: boolean error: Error | undefined}Initialize the agent and trigger authentication in the background:
await clientManager.initialize()This method:
Initialize only the HttpAgent (without authentication):
await clientManager.initializeAgent()Attempt to restore a previous authentication session:
const identity = await clientManager.authenticate()This dynamically imports the @icp-sdk/auth module. If the module isn’t installed, it fails gracefully.
Authenticate with Internet Identity:
await clientManager.login({ identityProvider: "https://identity.ic0.app", maxTimeToLive: BigInt(7 * 24 * 60 * 60 * 1_000_000_000), // 7 days onSuccess: () => console.log("Logged in!"), onError: (error) => console.error("Login failed:", error),})Clear authentication and reset to anonymous identity:
await clientManager.logout()This automatically invalidates all cached queries.
Update the agent’s identity and invalidate all queries:
clientManager.updateAgent(newIdentity)Get the current user’s Principal:
const principal = clientManager.getUserPrincipal()Subscribe to agent state changes:
const unsubscribe = clientManager.subscribeAgentState((state) => { console.log("Agent state:", state) // { isInitialized, isInitializing, isLocalhost, network, error }})
// Laterunsubscribe()Subscribe to authentication state changes:
const unsubscribe = clientManager.subscribeAuthState((state) => { console.log("Auth state:", state) // { identity, isAuthenticated, isAuthenticating, error }})
// Laterunsubscribe()Subscribe to identity changes:
const unsubscribe = clientManager.subscribe((identity) => { console.log("New identity:", identity.getPrincipal().toText())})Register a canister ID for tracking:
clientManager.registerCanisterId(canisterId, "backend")This is called automatically when creating a Reactor.
Get all registered canister IDs:
const canisterIds = clientManager.connectedCanisterIds()import { ClientManager, Reactor } from "@ic-reactor/core"import { QueryClient } from "@tanstack/query-core"import { idlFactory, type _SERVICE } from "../declarations/backend"
// Create QueryClient with defaultsexport const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 60_000, // 1 minute gcTime: 5 * 60_000, // 5 minutes retry: 3, }, },})
// Create ClientManagerexport const clientManager = new ClientManager({ queryClient, withProcessEnv: true, // Auto-detect network})
// Create Reactorexport const backend = new Reactor<_SERVICE>({ clientManager, idlFactory, canisterId: import.meta.env.VITE_BACKEND_CANISTER_ID,})// Auto-detect based on DFX_NETWORK environment variableconst clientManager = new ClientManager({ queryClient, withProcessEnv: true,})
// Force local development modeconst clientManager = new ClientManager({ queryClient, withLocalEnv: true, port: 8080, // Custom port})
// Custom agent optionsconst clientManager = new ClientManager({ queryClient, agentOptions: { host: "https://icp-api.io", verifyQuerySignatures: true, },})import { AuthClient } from "@icp-sdk/auth/client"
const authClient = await AuthClient.create()
const clientManager = new ClientManager({ queryClient, authClient, // Pass existing AuthClient})// In React with useEffectuseEffect(() => { const unsubAuth = clientManager.subscribeAuthState((state) => { if (state.isAuthenticated) { console.log("User logged in:", state.identity?.getPrincipal().toText()) } })
const unsubAgent = clientManager.subscribeAgentState((state) => { if (state.isInitialized) { console.log("Agent ready, network:", state.network) } })
return () => { unsubAuth() unsubAgent() }}, [])// Initialize on app startawait clientManager.initialize()
// Check if already authenticatedif (clientManager.authState.isAuthenticated) { console.log("Welcome back!", clientManager.getUserPrincipal().toText())}
// Login handlerasync function handleLogin() { await clientManager.login({ onSuccess: () => { console.log("Logged in!") // Queries are automatically invalidated }, onError: (error) => { console.error("Login failed:", error) }, })}
// Logout handlerasync function handleLogout() { await clientManager.logout() // Queries are automatically invalidated}const clientManager = new ClientManager({ queryClient, withProcessEnv: true,})
// Check current networkconsole.log(clientManager.isLocal) // true if localconsole.log(clientManager.network) // "local", "remote", or "ic"console.log(clientManager.agentHost?.toString()) // The host URLWhen authentication state changes (login/logout), ClientManager automatically invalidates all cached queries. This ensures:
// This happens automatically on identity change:queryClient.invalidateQueries()The @icp-sdk/auth package is an optional peer dependency. The auth module is only dynamically imported when you call login() or authenticate(). This keeps your bundle size small if you don’t need authentication.
# Only install if you need authenticationnpm install @icp-sdk/authWhile ClientManager includes built-in support for Internet Identity, it’s designed to work with any identity provider. You can integrate alternative authentication methods by updating the agent’s identity.
When using external authentication (wallets, SIWE, NFID, etc.), update the agent after authenticating:
// Authenticate with your external providerconst identity = await externalAuthProvider.getIdentity()
// Update ClientManager - all reactors automatically use the new identityclientManager.updateAgent(identity)This pattern works with any provider that produces a valid Identity object.
We’re exploring first-class support for popular authentication standards:
| Provider | Description | Status |
|---|---|---|
| SIWE | Sign-In With Ethereum — use MetaMask, WalletConnect, etc. | 🔮 Planned |
| SIWS | Sign-In With Solana — use Phantom, Solflare, etc. | 🔮 Planned |
Here’s how SIWE integration works with ic-siwe-js:
// 1. Wrap your app with SiweIdentityProviderimport { SiweIdentityProvider } from "ic-siwe-js/react"
function App() { return ( <SiweIdentityProvider canisterId={siweProviderCanisterId}> <YourApp /> </SiweIdentityProvider> )}
// 2. Use the useSiwe hook to login and get identityimport { useSiwe } from "ic-siwe-js/react"
function LoginButton() { const { login, identity, isLoggingIn } = useSiwe()
// After login, update ClientManager with the SIWE identity useEffect(() => { if (identity) { clientManager.updateAgent(identity) } }, [identity])
return ( <button onClick={login} disabled={isLoggingIn}> {isLoggingIn ? "Signing in..." : "Sign in with Ethereum"} </button> )}