⚠️ Alpha · testnets only. ChainRender is in active alpha and deployed on testnets only — Ethereum Sepolia, the Robinhood Chain Testnet, and the Circle Arc Testnet — not on any mainnet. Contracts, addresses, and APIs may change. Do not use with real funds or production assets.
Concepts

Building Correctly — Your App IS the On-Chain Component

The single most common mistake when building on ChainRender is treating it as a library a React app calls to produce a hash, instead of the framework the app is built in and rendered from. This page draws that line sharply, because getting it wrong produces an app that looks like ChainRender but isn't on-chain at all.

Building with an AI agent? Point it at the ChainRender MCP server & app-builder skill (@chainrender/mcp + the chainrender-app-builder SKILL.md). The skill teaches the correct compose → grade → compile → validate → render workflow and the anti-patterns below, so the agent builds a real on-chain app instead of a conventional dashboard. This is the recommended path for AI-assisted builds.

A complete, copyable example

This is the whole correct loop in one place. The balance is read and rendered on-chain by the Slot — you never fetch it yourself. Clone this shape (a full runnable version ships in the app-builder skill as examples/correct-react-app.tsx):

import { compileJsx } from "@chainrender/client";
import { renderComponent } from "@chainrender/client/runtime";

const DEPLOYMENT = { rpc: "<your-rpc>", router: "<RenderRouter>", registry: "<ComponentRegistry>" };

// 1. COMPILE — the Slot declares an on-chain read; the interpreter resolves it at render time.
const { hex, hash } = compileJsx(`
  <Box direction="column" variant="surface">
    <Text role="heading">Account Balance</Text>
    <Slot bind="balance" arg="0xd8dA...6045" />
  </Box>`);

// 2. REGISTER — write the bytes on-chain once (wallet tx to the ComponentRegistry with `hex`).
//    After this, the component lives on-chain forever, addressed by `hash`.

// 3. RENDER — one eth_call returns the fully on-chain-rendered HTML, balance included. Display THAT.
const html = await renderComponent(DEPLOYMENT, hash);
document.getElementById("app")!.innerHTML = html;   // ← the on-chain UI. Not hand-built React.

The balance in that output was read and rendered on-chain during step 3 — there is no getBalance call in your code. That is the difference between a ChainRender app and a viem dashboard.

Nuance: three things the ethos does NOT mean (avoid over-compliance)

Understanding "the component is the app" is the first step. Over-applying it produces an app that is rigid and barely functional. Three clarifications:

A client-side resolver is NOT Grade A — an on-chain resolver contract is

Routing data through a resolver does not make it on-chain. There are two kinds of resolver, and they grade differently:

  • Client-side resolver (a JS function you write that calls publicClient.getBlock() / getBalance(), e.g. the makeProviderResolvers helpers) — this is the CLIENT_FETCH path. The data is fetched off-chain in the browser and dropped into the slot. It is convenient and legitimate, but it grades C (Live), not A. Writing blockHeight: async () => publicClient.getBlock() is still an off-chain fetch — moving it into a resolver function does not make it trustless.
  • On-chain resolver contract (a resolver address in the Slot, e.g. <Slot resolver="0xEthBalanceResolver…" arg="0x…" />) — the value is read and rendered on-chain by that contract during the eth_call. This is the Grade A path.

So: for real trustlessness, bind a Slot to an on-chain resolver contract address, not a client-side JS function. Client-side resolvers are a pragmatic fallback (some data, like an arbitrary wallet's ENS name, is inherently client-side) — use them knowingly, and let the grade tell the truth (they land at C).

Ordinary client-side UI is fine — misrepresenting off-chain data as on-chain is the violation

The ethos does not forbid all React state. A unit converter that does arithmetic in the browser, form input state, tab switching, a wallet-connect button — these are ordinary client concerns and are perfectly fine as normal React/HTML. You do not need to turn a client-side calculator into an on-chain transaction to be "compliant." The actual violation is narrower: fetching on-chain data with viem and hand-rendering it as if it were rendered on-chain, while claiming the app is fully on-chain. Pure client-side computation isn't on-chain data and never claimed to be — so it's not a violation. Don't contort ordinary UI into on-chain submissions; just don't dress up off-chain data as on-chain.

Rendering requires a registered component — that's by design, not a bug

renderComponent / <ChainRenderView> render by hash via an eth_call, so the component must be registered on-chain first. There is no "render from raw source" — that's the whole point (the chain is the renderer). During authoring you preview with grade() / compileComponent() and the visual builder; to see the rendered component you register it (once) and then it renders for free forever. So "I have to register before the preview shows anything" is correct behavior, not a defect — budget one cheap registration per component.

The one idea to internalize

A ChainRender app is a component that is compiled to bytes, registered on-chain, and rendered by an eth_call. The component is the app. Your surrounding code (React, plain HTML, whatever) is a thin shell that calls renderComponent(...) and displays the on-chain HTML it returns — it does not re-implement the UI.

If your React code fetches data with viem and hand-renders it in JSX, and ChainRender is only used to print a hash on the side, you have not built a ChainRender app. You have built a conventional Web3 dashboard that imports ChainRender. The rendering is happening in your browser, not on-chain.

The correct flow: compile → register → render

import { compileJsx } from "@chainrender/client";
import { renderComponent } from "@chainrender/client/runtime";

// 1. COMPILE — turn your component into on-chain bytes + a content hash.
const { hex, hash } = compileJsx(`
  <Box direction="column" variant="surface">
    <Text role="heading">Account Balance</Text>
    <Slot bind="balance" arg="0xd8dA...6045" />
  </Box>
`);

// 2. REGISTER — store the bytes on-chain, ONCE. (Uses your wallet + the ComponentRegistry.)
//    After this, the component lives on-chain forever, addressed by `hash`.
//    (See the registry helpers; this is a one-time write, not part of rendering.)

// 3. RENDER — anyone renders it for free with a single eth_call. The HTML is computed ON-CHAIN.
const html = await renderComponent({ rpc, router }, hash);
// `html` is the fully-rendered component, including the live balance the Slot resolved on-chain.
document.getElementById("app").innerHTML = html;

Step 3 is the whole point: the balance in that Slot is read and rendered on-chain during the eth_call. You do not fetch the balance yourself and paste it into JSX — the on-chain interpreter does it, and the result is identical against any RPC, with no server.

The anti-patterns (what "wrong" looks like)

These are the exact mistakes that turn a ChainRender app into a conventional dashboard. Avoid all of them:

❌ Fetching on-chain data with viem and hand-rendering it

// WRONG — this is a normal React app, not ChainRender.
const bal = await publicClient.getBalance({ address });   // off-chain fetch
return <div>{formatEther(bal)} ETH</div>;                 // hand-rendered in React
// RIGHT — declare a Slot; the on-chain interpreter reads + renders the balance.
compileJsx(`<Slot bind="balance" arg="${address}" />`);   // → register → renderComponent

The difference: the wrong version renders in your browser (nothing on-chain); the right version renders on-chain and returns HTML. If you're calling publicClient.getBalance/readContract and dropping the result into JSX, you've bypassed ChainRender entirely.

❌ Compiling but never registering

compileJsx gives you a hash of bytes that do not exist on-chain yet. A hash of un-deployed bytes is not an on-chain app — it's a preview. A real ChainRender app registers the bytes (step 2) so renderComponent can find them. If you only ever call compileJsx and display the hash, nothing is on-chain.

❌ Importing the render component but never rendering with it

Importing ChainRenderView / renderComponent and then only displaying compiled.hash as text — with the render component absent from your returned JSX — means nothing renders. The app becomes a compiler-output viewer with no live UI and no interactivity. You must actually place <ChainRenderView deployment={...} hash={...} resolvers={...} /> in your JSX (and register the hash on-chain), or call renderComponent and insert the returned HTML. Importing it is not using it.

❌ Displaying the hash instead of rendering the component

Showing "Content Hash: 0x…" in a sidebar is a nice debug view, but it is not using ChainRender. The app should render the component (step 3) and show that — the actual on-chain-rendered UI — as the primary content.

❌ Reaching for an off-chain indexer/API

Fetching contract source from a block-explorer API, prices from a REST endpoint, or data from any Web2 service re-introduces the off-chain trust ChainRender exists to remove. If you need on-chain data, read it with a resolver inside a Slot, so it's rendered on-chain and grades honestly. If you genuinely need an off-chain source, it must be declared (provenance: "external" / CLIENT_FETCH) so the grade discloses it — it cannot silently masquerade as on-chain.

The litmus test

Ask these about your app. A genuine ChainRender app answers "yes" to all:

  1. Is the UI a compiled ChainRender component (not hand-written React/HTML that mirrors it)?
  2. Are the component's bytes registered on-chain (not just compiled to a hash)?
  3. Is the UI produced by renderComponent / an eth_call (not assembled in the browser)?
  4. Is on-chain data read via Slot + resolver (not fetched with viem and pasted in)?
  5. Does the trust grade reflect reality (off-chain dependencies declared, not hidden)?

If any answer is "no," you've built a conventional Web3 app that uses ChainRender, not a ChainRender app. That's a valid thing to build — but it is not on-chain, and it must not be presented as such.

Where React fits (correctly)

React is welcome — as the shell, not the renderer. The correct React usage: a component calls renderComponent, gets on-chain HTML, and displays it (see Using React and the @chainrender/client/react helpers). React handles routing, wallet connection, and chrome; ChainRender handles the actual UI, rendered on-chain. React should never re-implement what the component renders.

Related