Skip to content
IC Reactor

TanStack Router

A full-featured application demonstrating IC Reactor with TanStack Router for navigation, route loaders, and authentication.

  • One shared reactor that a route loader points at the route’s canister with setCanisterId()
  • Hand-maintained createQuery / createSuspenseQuery / createSuspenseQueryFactory / createMutation modules, one per method
  • Suspense-based data fetching with <Suspense> boundaries
  • Query invalidation after mutations: the transfer mutation lists the balance query factory in invalidateQueries, and a refresh button calls reactor.invalidateQueries()
  • Authentication with createAuthHooks
  • File-based routing structure

A single DisplayReactor is defined once, and every query object below is built on it. The demo’s route loader points it at the token the route names:

src/canisters/ledger/reactor.ts
import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
import { clientManager } from "../../lib/client"
import { idlFactory, type _SERVICE } from "./declarations/icrc1.did"
export type LedgerService = _SERVICE
export const ledgerReactor = new DisplayReactor<LedgerService>({
clientManager,
idlFactory,
name: "ledger",
canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
})
export const { useActorQuery, useActorMutation, useActorSuspenseQuery } =
createActorHooks(ledgerReactor)

The demo’s loader points the shared reactor at the canister for this route. Query keys are rooted at the reactor’s canister ID, so every cached entry is scoped automatically:

// src/routes/wallet/$canisterId.tsx
import { createFileRoute } from "@tanstack/react-router"
import { ledgerReactor } from "@/canisters/ledger/reactor"
export const Route = createFileRoute("/wallet/$canisterId")({
component: TokenWallet,
loader: async ({ params: { canisterId } }) => {
ledgerReactor.setCanisterId(canisterId)
},
})
function TokenWallet() {
// Refresh every query for the active canister
const handleRefreshAll = () => ledgerReactor.invalidateQueries()
return <button onClick={handleRefreshAll}>↻ Refresh</button>
}

The demo does not use forCanister() or callConfig. For new code, the same token name reads with a canister of its own instead of a retargeted reactor:

// Not in the demo: a token name from a canister of its own, with `callConfig`
import { useActorQuery } from "@/canisters/ledger/reactor"
export function TokenName({ canisterId }: { canisterId: string }) {
const { data: name } = useActorQuery({
functionName: "icrc1_name",
callConfig: { canisterId },
})
return <span>{name ?? "Token"}</span>
}

The modules in src/canisters/ledger/hooks/ wrap the reactor in reusable query objects, usable both inside components and in loaders. They are hand-maintained because this example writes its reactor by hand. In a project whose reactor codegen generates, factories: true generates the non-suspense ones, such as icrc1NameQuery below and icrc1TransferMutation, under the same names, as in Codegen in Action. It generates no suspense factories such as icrc1BalanceOfSuspenseQuery.

src/canisters/ledger/hooks/icrc1NameQuery.ts
import { createQuery } from "@ic-reactor/react"
import { ledgerReactor } from "../reactor"
export const icrc1NameQuery = createQuery(ledgerReactor, {
functionName: "icrc1_name",
})
src/components/token-name.tsx
import { icrc1NameQuery } from "@/canisters/ledger/hooks"
export function TokenName() {
const { data: name } = icrc1NameQuery.useQuery()
return <span>{name ?? "Token"}</span>
}