Skip to content
IC Reactor

Next.js SSR

A Next.js App Router app built on @ic-reactor/core and @ic-reactor/react with TanStack Query’s own hooks. It reads the ICP, ckBTC and ckETH ledgers on mainnet through one module generated from icrc1.did by candid-core-cli gen, and shows the server-rendering story end to end:

  • One client per request. A Server Component builds createClient({ network: "ic", identity: "anonymous" }) inside the request (React’s cache()), prefetches every ledger’s name, symbol, decimals, fee, total supply, minting account and metadata with { id } targets, and hands dehydrate(client.queryClient) to a <HydrationBoundary>.
  • The provider factory in a 'use client' module. <ReactorProvider client={() => createClient({ network: "ic", auth: () => new AuthClient() })}> builds a client per mounted tree. The server render and the browser’s hydrating render build keys for the anonymous principal, so every visitor gets the server’s keys, a tab still signed in from an earlier visit included, and the cards hydrate from them with nothing fetched on load.
  • Lossless hydration. bigint amounts, principals and Uint8Array values arrive exact; the page prints each value’s run-time type.
  • No JavaScript needed to read the pages: the values are in the server’s HTML, which a smoke script checks against a running next start.
  • Streaming. On the account page, certified balances (replicated calls) stream in after the rest of the page, from an async Server Component inside <Suspense>.
  • Progressive enhancement. The account lookup is a GET form validated on the server with isPrincipal and principal().
  • A Route Handler with its own client per request answers JSON, with amounts as decimal text and base units, and maps a ReactorError to an HTTP status by its kind, reading httpStatus where the kind is not enough: a canister that does not exist is a 502, not a 503 that invites a retry.
  • Errors on the server. A canister that is not a ledger renders its own section with the error’s kind; the rest of the page renders as usual.
  • Sign-in after hydration. Signing in with Internet Identity moves every read to keys that hold the user’s principal; nothing read anonymously is reused for them. A tab that reloads signed in hydrates without a mismatch, then reads its own keys the same way.

The tests run the real client on createTestClient() from @ic-reactor/core/testing, with the ledgers mocked over in-memory replicas, and never reach mainnet.

The SSR and hydration guide explains each rule, and Auth covers sign-in and the caller.

StackBlitz installs the example from npm and runs the tests (npm test): you can read and change the code there and run them again. It cannot serve the app. Next 16 keeps each request’s state in Node’s AsyncLocalStorage, which StackBlitz does not implement, so next dev fails there on every page with Next’s own “Invariant: Expected workStore to be initialized”, and a production build renders through the same storage. Clone the repository to run the app.