Skip to content
IC Reactor

Local Development

The modern way to handle local development is using icp-cli and @ic-reactor/vite-plugin. This setup automates environment variables and proxy configuration.

If you’re using Vite, add the plugin to your vite.config.ts. It will automatically proxy /api requests to your local replica and inject canister IDs.

vite.config.ts
import { defineConfig } from "vite"
import { icReactor } from "@ic-reactor/vite-plugin"
export default defineConfig({
plugins: [
icReactor({
canisters: [
{
name: "backend",
didFile: "src/declarations/backend.did",
},
],
}),
],
})

The ClientManager automatically detects your environment through the ic_env cookie set by the Vite plugin or an asset canister.

Put it where the generated reactors expect it. The plugin’s default clientManagerPath is "../../clients", and its default outDir is src/declarations, so the generated src/declarations/backend/index.generated.ts resolves clientManager from src/clients.ts:

src/clients.ts
import {
AuthenticationManager,
ClientManager,
createAuthHooks,
} from "@ic-reactor/react"
import { QueryClient } from "@tanstack/react-query"
export const queryClient = new QueryClient()
// Network and local canister IDs come from the ic_env cookie automatically
export const clientManager = new ClientManager({ queryClient })
export const authentication = new AuthenticationManager({ clientManager })
export const { useAuth, useAgentState } = createAuthHooks(authentication)
  1. Start the local network:

    Terminal window
    icp network start -d
  2. Deploy your backend:

    Terminal window
    icp deploy backend
  3. Start your dev server:

    Terminal window
    npm run dev

If you’re not using the Vite plugin, you can still configure everything manually.

ClientManagerParameters has four members: the required queryClient, an optional agentOptions (the HttpAgentOptions handed to the underlying HttpAgent), an optional allowEnvConfig, and allowEnvRootKey — the former name of that option, still honoured for the root key alone. Network detection needs no flag — it always runs. allowEnvConfig controls whether the ic_env cookie is trusted at all: its root key, its Internet Identity provider, and the canister IDs a reactor resolves by name. It defaults to true only when both the agent host and the page are unambiguously a local replica, and false everywhere else, so a reactor with no configured canisterId works on localhost and throws on a real domain. The root key is the exception: with neither option set, it is judged by the agent host alone, so a page on a real domain pointed at a local replica still takes IC_ROOT_KEY from the cookie. Pass allowEnvConfig: false to refuse it.

import { ClientManager } from "@ic-reactor/react"
import { QueryClient } from "@tanstack/react-query"
export const queryClient = new QueryClient()
// Served from localhost → the host is inferred from window.location.origin
export const clientManager = new ClientManager({ queryClient })

To point the agent somewhere else, pass the host explicitly:

import { ClientManager } from "@ic-reactor/react"
import { QueryClient } from "@tanstack/react-query"
export const queryClient = new QueryClient()
export const clientManager = new ClientManager({
queryClient,
agentOptions: { host: "http://127.0.0.1:4943" },
})
.env.local
VITE_BACKEND_CANISTER_ID=bkyz2-fmaaa-aaaaa-qaaaq-cai
import { Reactor } from "@ic-reactor/react"
import { idlFactory, type _SERVICE } from "./declarations/backend"
export const backend = new Reactor<_SERVICE>({
clientManager,
idlFactory,
name: "backend",
canisterId: import.meta.env.VITE_BACKEND_CANISTER_ID,
})

A dev server forwarded by GitHub Codespaces (*.app.github.dev) or Gitpod (*.gitpod.io) works like one on localhost: with no agentOptions.host, the agent routes through the page’s own origin, network is "remote", and the agent fetches the replica’s root key. Your dev server has to proxy /api to the replica, as @ic-reactor/vite-plugin does.

The ic_env cookie is not trusted there. Every workspace is a subdomain of a domain shared with every other user of the platform, so a page in a stranger’s workspace can set the cookie for yours. Give reactors their canister IDs explicitly (the codegen canisterId option), or pass allowEnvConfig: true to ClientManager if you accept that risk for a development build:

import { ClientManager } from "@ic-reactor/react"
import { QueryClient } from "@tanstack/react-query"
export const queryClient = new QueryClient()
export const clientManager = new ClientManager({
queryClient,
// Codespaces/Gitpod only: trust the ic_env cookie's canister IDs, root key
// and Internet Identity provider despite the shared parent domain.
allowEnvConfig: import.meta.env.DEV,
})

Internet Identity sign-in needs a provider URL the browser can reach. Off mainnet, AuthenticationManager derives a local one (http://<canister>.localhost:<port>/authorize), and the plugin’s cookie carries the same kind of URL. Neither resolves in a browser outside the workspace, so pass identityProvider and internetIdentityId to AuthenticationManager for an Internet Identity your browser can open (see Authentication).

When communicating with the IC mainnet, the agent uses a hardcoded public key to verify responses. For a local replica, the agent needs to fetch a different root key.

import { useEffect } from "react"
import { clientManager, useAgentState } from "./clients"
function App() {
const { isInitialized } = useAgentState()
useEffect(() => {
clientManager.initialize()
}, [])
if (!isInitialized) {
return <div>Loading...</div>
}
return <YourApp />
}

This error usually means the agent checked a local replica’s certificate against mainnet’s root key. IC Reactor fetches the local key only when the agent host is a local (or Codespaces/Gitpod) host.

Solution: Make sure the agent points at the local replica: serve the page from localhost/127.0.0.1 or a Codespaces or Gitpod domain, or set agentOptions.host to the replica’s local address.

Check if your local replica is running and on which port.

  1. If you use icp-cli, run icp network status -e local --json and check the returned port.

  2. If you use a manual dfx workflow, run dfx info webserver-port.

  3. If the replica is not on 4943, point the agent at it through agentOptions.host:

    import { ClientManager } from "@ic-reactor/react"
    import { QueryClient } from "@tanstack/react-query"
    export const queryClient = new QueryClient()
    export const clientManager = new ClientManager({
    queryClient,
    agentOptions: { host: "http://127.0.0.1:8000" },
    })

Internet Identity on Localhost (manual dfx workflow)

Section titled “Internet Identity on Localhost (manual dfx workflow)”

If you are using a manual dfx project, deploy a local instance of the Internet Identity canister.

  1. Pull the dependency in dfx.json:

    {
    "canisters": {
    //...
    "internet_identity": {
    "type": "custom",
    "candid": "https://github.com/dfinity/internet-identity/releases/download/release-2026-03-16/internet_identity.did",
    "wasm": "https://github.com/dfinity/internet-identity/releases/download/release-2026-03-16/internet_identity_dev.wasm.gz",
    "specified_id": "rdmx6-jaaaa-aaaaa-aaadq-cai"
    }
    }
    }
  2. Deploy it:

    It is recommended to deploy it to the default canister ID:

    Terminal window
    dfx deploy internet_identity --specified-id rdmx6-jaaaa-aaaaa-aaadq-cai
  3. Login using the local provider URL. useAuth handles this automatically once the local Internet Identity canister is deployed and the page is served from localhost.

    import { useAuth } from "./clients"
    function LoginButton() {
    // Auto-detects based on network
    // Local: http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:4943/authorize
    // Mainnet: https://id.ai/authorize
    const { login } = useAuth()
    return <button onClick={() => login()}>Login</button>
    }