@ic-reactor/vite-plugin
@ic-reactor/vite-plugin runs the shared
@ic-reactor/codegen pipeline inside Vite. It
generates declarations and typed reactor entrypoints, with optional React
hooks, from .did files, watches for changes, and can inject the ic_env
cookie used by
ClientManager.
Installation
Section titled “Installation”pnpm add -D @ic-reactor/vite-pluginpnpm add @ic-reactor/react @tanstack/react-query @icp-sdk/coreFor non-React output, set target: "core" and install the matching runtime
package instead of @ic-reactor/react.
import { defineConfig } from "vite"import react from "@vitejs/plugin-react"import { icReactor } from "@ic-reactor/vite-plugin"
export default defineConfig({ plugins: [ react(), icReactor({ canisters: [{ name: "backend", didFile: "./backend/backend.did" }], }), ],})By default, generated files go to src/declarations/<canister>/, and the
plugin imports clientManager from ../../clients.
Relative outDir and didFile paths resolve against Vite’s resolved
config.root, not the directory you ran vite from. If you set
root: "frontend" or pass the root positionally (vite build apps/web), paths
are relative to that directory. Note that --config on its own only selects the
config file — it does not change the root.
For each canister, generation produces:
declarations/<did-basename>.js,.d.ts, and a.didcopyindex.generated.ts: managed implementation regenerated on every runindex.factories.generated.ts: managed query and mutation factories, only for a canister that setsfactories: trueindex.ts: stable entry wrapper that re-exports fromindex.generated.ts, and fromindex.factories.generated.tswhen it is generated
If Prettier resolves from config.root, the plugin formats the .js, .d.ts,
index.generated.ts, index.factories.generated.ts and the index.ts wrapper
it writes with it and your
Prettier config, so a rebuild leaves formatted, committed output unchanged.
Without Prettier the declarations hold the Candid parser’s output followed by
a newline, and a formatting error never fails the build.
With the default target: "react", index.generated.ts exports the reactor
plus hooks named after the canister — for a canister named backend:
useBackendQuery, useBackendSuspenseQuery, useBackendInfiniteQuery,
useBackendSuspenseInfiniteQuery, useBackendMutation, and
useBackendMethod.
Query and Mutation Factories
Section titled “Query and Mutation Factories”Set factories: true on a canister to also generate
index.factories.generated.ts, with one object per method bound to the
generated reactor: createQuery for a query method without arguments,
createQueryFactory for one with arguments, and createMutation for an
update or oneway method. Each is named after its method in camelCase with a
Query or Mutation suffix.
import { icReactor } from "@ic-reactor/vite-plugin"
icReactor({ canisters: [ { name: "backend", didFile: "./backend/backend.did", factories: true }, ],})import { getMessageQuery, setMessageMutation } from "./declarations/backend"
// In a componentfunction Message() { const { data } = getMessageQuery.useQuery()
return <p>{data}</p>}
// Anywhere, outside React includedasync function saveMessage() { await setMessageMutation.execute(["hello"])}The file is regenerated with the rest of the canister’s bindings when its
.did changes. The default index.ts wrapper re-exports it; if you have
edited your index.ts, the plugin leaves it alone and warns in the terminal
until it re-exports the factories. Setting factories back to false
removes the file. It needs target: "react". See
Query and Mutation Factories
for the naming rule for any method name and how to customize a factory.
Plugin Options
Section titled “Plugin Options”| Option | Type | Default | Description |
|---|---|---|---|
canisters |
CanisterConfig[] |
— | Canister configurations (required). |
outDir |
string |
"src/declarations" |
Base output directory for generated files. |
clientManagerPath |
string |
"../../clients" |
Default client manager import path (relative from generated files). |
target |
"react" | "core" |
"react" |
Default generated runtime target. |
injectEnvironment |
boolean |
true |
Inject ic_env cookie and proxy during vite dev and vite preview. |
failOnError |
boolean |
true on vite build, false on vite dev |
Abort the Vite run when a canister fails to generate. |
Per-canister options
Section titled “Per-canister options”| Option | Type | Description |
|---|---|---|
name |
string |
Canister name (required). |
didFile |
string |
Path to the .did file (required). |
mode |
string |
Reactor class: Reactor, DisplayReactor (default), CandidReactor, CandidDisplayReactor, MetadataDisplayReactor. |
outDir |
string |
Override output directory for this canister. |
clientManagerPath |
string |
Override client manager path for this canister. |
target |
string |
Override runtime target for this canister. |
canisterId |
string |
Canister ID written into index.generated.ts. Needed for any build not served from a local replica; see below. |
factories |
boolean |
Also generate index.factories.generated.ts, a query or mutation object per method (default false). Needs target: "react". |
Each entry needs an output directory of its own. Two entries with the same
name and no outDir, or with outDir values that reach one directory, would
overwrite each other’s output. The plugin generates the first of them and fails
the later one with the error the CLI reports, which names both entries by their
position in canisters. Under vite build that fails the build; under
vite dev it goes to the terminal and the browser error overlay. To generate
one canister twice, say as a DisplayReactor and as a Reactor, give each
entry its own outDir.
Reactor Mode
Section titled “Reactor Mode”Set mode per canister to choose the generated reactor class:
import { icReactor } from "@ic-reactor/vite-plugin"
icReactor({ canisters: [ { name: "backend", didFile: "./backend/backend.did" }, { name: "workflow_engine", didFile: "./workflow/workflow_engine.did", mode: "Reactor", }, ],})Supported modes:
ReactorDisplayReactorCandidReactorCandidDisplayReactorMetadataDisplayReactor
Set target to control whether generated files include React hooks:
react(default): generates the reactor plus bound React hookscore: generates only the typed reactor exports with no React dependency
Local Development
Section titled “Local Development”When injectEnvironment is enabled during vite dev or vite preview, the
plugin tries to read local network details from the icp CLI, then:
- resolves canister IDs (including
internet_identity, which is added automatically if not already in your canister list) - sets the
ic_envcookie on each response - proxies
/apito the local replica
If icp CLI environment detection is unavailable, the plugin falls back to
proxying /api to http://127.0.0.1:4943, and sets no cookie. With no
canisters configured it still sets one, naming only icp-cli’s built-in Internet
Identity at http://id.ai.localhost:8000/authorize.
Detection is complete once icp reports the network and every configured
canister has an ID. Until then the plugin asks icp again on each page load,
and that page gets the answer, so you can start vite dev first and then run
icp network start and icp deploy: reload the page, no restart needed. The
/api proxy moves to the network icp reports, the fallback included. Once
detection is complete, page loads run no further icp commands, so
redeploying into a fresh network, with new canister IDs and a new root key,
needs a dev server restart. The terminal warns at startup while detection is
incomplete, and says when a later page load completes it. A configured canister
you never deploy locally keeps detection incomplete, so every page load runs
icp for it; set its canisterId and it counts as resolved. If you never run a
local network, set injectEnvironment: false and page loads run no icp.
If your Vite config or another plugin sets server.proxy["/api"], the plugin
leaves that entry alone, whether detection succeeds or not, and that proxy does
not follow detection.
If you provide canisterId in the plugin config, that fixed ID overrides the
auto-detected development value for that canister.
ClientManager trusts the ic_env cookie only on a local replica: cookies are
not origin-isolated, so on a custom domain, on mainnet, and on Codespaces or
Gitpod (whose workspaces share a parent domain with every other user’s) the
cookie is ignored.
There, and in Node or during SSR where there is no cookie at all, a generated
reactor with no canisterId throws canisterId is required as soon as its
module is imported. Set the per-canister canisterId to write the ID into
index.generated.ts for those builds, or pass allowEnvConfig: true to
ClientManager if you trust every subdomain of the domain you serve from.
Set the ICP_ENVIRONMENT environment variable to target a non-default network
(defaults to "local").
.did File Changes
Section titled “.did File Changes”When a watched .did file changes, the plugin:
- regenerates declarations
- regenerates
index.generated.ts, andindex.factories.generated.tsfor a canister that setsfactories: true - preserves custom
index.tsfiles, while updating the default wrapper or migrating older generated entrypoints to the wrapper model - triggers a full browser reload
vite build --watch watches the configured .did files as well. Saving one
starts a rebuild that regenerates that canister’s bindings. A rebuild that any
other file starts leaves the generated files alone.
A generation failure aborts vite build by default — a build that silently
ships the bindings from the last successful run is worse than no build. Under
vite dev the default is the opposite, because the dev server has to survive
the broken intermediate states of a .did being edited; there the failure is
reported to the terminal and to the browser error overlay instead. Set
failOnError explicitly to override either default.
The plugin follows the dev server’s file watcher itself, so a .did file that
appears after the server started, or that a build tool deletes and writes
again, is generated too, and saves regenerate even with server.hmr: false.