Skip to content
IC Reactor

Custom Provider

This example demonstrates how to build a Token Viewer where the user can input any Canister ID, and the app dynamically connects to it. A provider built with createReactorProvider instantiates a Reactor from its props and hands its hooks to the components below it.

  • Dynamic Canister IDs: Connecting to canisters determined at runtime.
  • Props for the factory: createReactorProvider passes the provider’s props to the factory that builds the reactor.
  • Reusability: A generic ICRC1Provider that can wrap any part of the app, typed through useICRC1Context() without a hand-written context.
src/ICRC1Provider.tsx
import {
Reactor,
createActorHooks,
createReactorProvider,
} from "@ic-reactor/react"
import { idlFactory, type ICRC1 } from "./declarations/icrc1"
import { clientManager } from "./reactor"
const { ReactorProvider, useReactor } = createReactorProvider(
({ canisterId }: { canisterId: string }) => {
const reactor = new Reactor<ICRC1>({
name: "icrc1",
clientManager,
canisterId,
idlFactory,
})
return { canisterId, hooks: createActorHooks(reactor) }
}
)
export const useICRC1Context = useReactor
export default ReactorProvider

The factory runs once, when the provider mounts. App gives the provider a key, so a new canister ID is a new provider and the factory builds a reactor for it:

// src/App.tsx (simplified: the demo's App also renders the token cards and login)
import { useState, type FormEvent } from "react"
import { isPrincipalText } from "@ic-reactor/react"
import ICRC1Call from "./ICRC1Call"
import ICRC1Provider from "./ICRC1Provider"
export default function App() {
const [canisterId, setCanisterId] = useState("ryjl3-tyaaa-aaaaa-aaaba-cai")
const [draft, setDraft] = useState(canisterId)
// Only text that is a principal becomes the canister ID, so a half-typed
// value never reaches the reactor factory.
const onSubmit = (event: FormEvent) => {
event.preventDefault()
const text = draft.trim()
if (isPrincipalText(text)) {
setCanisterId(text)
} else {
console.error(`Not a canister ID: ${text}`)
}
}
return (
<>
<form onSubmit={onSubmit}>
<input
value={draft}
onChange={(event) => setDraft(event.target.value)}
/>
<button type="submit">Fetch Token</button>
</form>
<ICRC1Provider key={canisterId} canisterId={canisterId}>
<ICRC1Call functionName="icrc1_symbol" label="Symbol" />
</ICRC1Provider>
</>
)
}

Components read the hooks with useICRC1Context(), typed as the factory’s return value, so hooks.useActorQuery checks functionName against the ICRC1 service. Every reactor shares the app’s ClientManager, so one sign-in covers every token, and each canister’s queries are cached under its own ID.