TanStack Router
A full-featured application demonstrating IC Reactor with TanStack Router for navigation, route loaders, and authentication.
Features Demonstrated
Section titled “Features Demonstrated”- One shared reactor that a route
loaderpoints at the route’s canister withsetCanisterId() - Hand-maintained
createQuery/createSuspenseQuery/createSuspenseQueryFactory/createMutationmodules, 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 callsreactor.invalidateQueries() - Authentication with
createAuthHooks - File-based routing structure
Live Preview
Section titled “Live Preview”Key Code
Section titled “Key Code”One Shared Reactor
Section titled “One Shared Reactor”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:
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)Route Loader
Section titled “Route Loader”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.tsximport { 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>}Per-Method Query Modules
Section titled “Per-Method Query Modules”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.
import { createQuery } from "@ic-reactor/react"import { ledgerReactor } from "../reactor"
export const icrc1NameQuery = createQuery(ledgerReactor, { functionName: "icrc1_name",})import { icrc1NameQuery } from "@/canisters/ledger/hooks"
export function TokenName() { const { data: name } = icrc1NameQuery.useQuery() return <span>{name ?? "Token"}</span>}