FRONTEND

React

Bind the hooks to your client once. Every call site is then typed off your ABI.

React context cannot be generic, so the hooks are not free functions you import from the package. You bind them to your client once, next to where you created it, and export the result. Every call site then knows your ABI. The example plays the public Room, so its client is the Room’s: for your app, the pair ship printed and your ABI, as in the first hour’s lib/interlude.ts.

lib/demo.ts
import { createPublicClient, http } from "viem";
import { monadTestnet } from "viem/chains";
import { createInterludeClient, roomAbi } from "@interludelayer-sdk/sdk";

// The public Room (eu floor) and the node that serves it, and no other contract.
export const interlude = createInterludeClient({
  app: "0xA116fcaEC711c9D8f64DA409CBE3AF673Fb9084C",
  abi: roomAbi,
  node: "https://rpc.interludelayer.xyz",
  base: createPublicClient({
    chain: monadTestnet,
    transport: http("https://testnet-rpc.monad.xyz"),
  }),
});
app/game.tsx
import type { WalletClient } from "viem";
import { createInterludeHooks } from "@interludelayer-sdk/sdk/react";
import { interlude } from "@/lib/demo";

export const { InterludeProvider, useSession, useSessionCall, useRead, useWatch } =
  createInterludeHooks(interlude);

function Floor() {
  const { session, open, isOpening } = useSession();
  const join = useSessionCall("join");
  const move = useSessionCall("move");
  const { data: taken } = useWatch("takenCount"); // re-read on every call the node applies

  if (!session) {
    return (
      <button onClick={() => open()}>
        {isOpening ? "Check your wallet" : "Enter"}
      </button>
    );
  }

  return (
    <>
      <p>{taken === undefined ? "…" : `${taken} on the floor`}</p>
      <button onClick={() => join.send()}>Join</button>
      <button onClick={() => move.send([1])}>East</button>
    </>
  );
}

export function Demo({ wallet }: { wallet: WalletClient | undefined }) {
  return (
    <InterludeProvider wallet={wallet} scope={["join", "move", "jump", "hit", "leave"]} ensureChain>
      <Floor />
    </InterludeProvider>
  );
}

What each hook is for

useSession is the grant. open() is openSession with the wallet and scope you put on the provider. useSessionCall("move") is one function on that grant: .send(), the last .data, .error, .isPending, .latencyMs. useRead is the live node read, same arguments as interlude.read, polled if you pass { pollMs }. useWatch takes the same arguments and re-reads each time the node applies a call, over one WebSocket for the whole page (Live vs settled). useNodeStatus() polls status() every 2 s, and useInterlude() hands back the client.

On the provider, wallet may be undefined until one is connected (wagmi’s useWalletClient().data, as is). ensureChain switches the wallet to Monad testnet before it signs. autoRenew signs a fresh grant a minute before the current one expires, so a long game does not stop on SessionExpiredError; it is off by default because a browser wallet prompts for it.

useSessionCall(...).send never rejects: on failure it resolves to undefined and puts the error in .error, so a bare onClick cannot produce an unhandled rejection, and a try/catch around it never catches anything. Call session.send directly where you want the throw.

Every hook and option is in the SDK’s README on npm.