Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

React Hooks

The SDK provides React hooks for declarative data fetching. All hooks return { data, isLoading, error, refetch } and handle loading states automatically.

Setup

Wrap your app with DenshokanProvider:

import { DenshokanProvider } from "@provable-games/denshokan-sdk/react";
 
function App() {
  return (
    <DenshokanProvider config={{ chain: "mainnet" }}>
      <MyApp />
    </DenshokanProvider>
  );
}

Provider Props

interface DenshokanProviderProps {
  children: ReactNode;
  config?: DenshokanClientConfig;  // Create a new client from config
  client?: DenshokanClient;        // Or pass an existing client
}

You can pass either config (and a client will be created) or client (to reuse an existing instance).

Accessing the Client

import { useDenshokanClient } from "@provable-games/denshokan-sdk/react";
 
function MyComponent() {
  const client = useDenshokanClient();
  // Use client for custom queries or write operations
}

Data Hooks

Games

import { useGames, useGame, useGameStats } from "@provable-games/denshokan-sdk/react";
 
function GameList() {
  const { data: games, isLoading } = useGames({ limit: 20, offset: 0 });
 
  return games?.data.map(game => (
    <div key={game.gameId}>{game.name}</div>
  ));
}
 
function GameDetail({ address }: { address: string }) {
  const { data: game } = useGame(address);
  const { data: stats } = useGameStats(address);
 
  return (
    <div>
      <h1>{game?.name}</h1>
      <p>{stats?.totalTokens} tokens minted</p>
      <p>{stats?.uniquePlayers} unique players</p>
    </div>
  );
}

Tokens

import { useTokens, useToken, useTokenScores } from "@provable-games/denshokan-sdk/react";
 
function TokenList({ gameAddress }: { gameAddress: string }) {
  const { data: tokens, isLoadingUri } = useTokens({
    gameAddress,
    gameOver: true,
    includeUri: true,
    limit: 50,
  });
 
  return (
    <div>
      {isLoadingUri && <p>Loading token images...</p>}
      {tokens?.data.map(token => (
        <div key={token.tokenId}>
          Score: {token.score}
          {token.tokenUri && <img src={token.tokenUri} />}
        </div>
      ))}
    </div>
  );
}
 
function TokenDetail({ tokenId }: { tokenId: string }) {
  const { data: token, isLoading } = useToken(tokenId);
  const { data: scores } = useTokenScores(tokenId, 10);
 
  if (isLoading) return <div>Loading...</div>;
 
  return (
    <div>
      <p>Score: {token?.score}</p>
      <p>Game Over: {token?.gameOver ? "Yes" : "No"}</p>
      <p>Playable: {token?.isPlayable ? "Yes" : "No"}</p>
    </div>
  );
}

Players

import { usePlayerStats, usePlayerTokens } from "@provable-games/denshokan-sdk/react";
 
function PlayerProfile({ address }: { address: string }) {
  const { data: stats } = usePlayerStats(address);
  const { data: tokens } = usePlayerTokens(address, {
    gameAddress: "0x1234...",
  });
 
  return (
    <div>
      <p>Total Tokens: {stats?.totalTokens}</p>
      <p>Games Played: {stats?.gamesPlayed}</p>
      {tokens?.data.map(t => <TokenCard key={t.tokenId} token={t} />)}
    </div>
  );
}

Minters

import { useMinters } from "@provable-games/denshokan-sdk/react";
 
function MinterList() {
  const { data: minters, isLoading } = useMinters({ limit: 20, offset: 0 });
 
  if (isLoading) return <div>Loading...</div>;
 
  return minters?.data.map(minter => (
    <div key={minter.id}>
      {minter.name}{minter.contractAddress}
    </div>
  ));
}

Token Decoding (Client-side)

Decode a packed token ID without any network call:

import { useDecodeToken } from "@provable-games/denshokan-sdk/react";
 
function TokenInfo({ tokenId }: { tokenId: string }) {
  const decoded = useDecodeToken(tokenId);
 
  if (!decoded) return null;
 
  return (
    <div>
      <p>Game ID: {decoded.gameId}</p>
      <p>Settings: {decoded.settingsId}</p>
      <p>Minted: {decoded.mintedAt}</p>
      <p>Soulbound: {decoded.soulbound ? "Yes" : "No"}</p>
    </div>
  );
}

useDecodeToken returns a CoreToken directly (no loading state) since decoding is purely client-side.

RPC Hooks

Direct contract reads:

import {
  useBalanceOf,
  useOwnerOf,
  useTokenUri,
  useTokenUriBatch,
  useTokenMetadataBatch,
  useScoreBatch,
  useGameOverBatch,
  useObjectivesCount,
  useSettingsCount,
} from "@provable-games/denshokan-sdk/react";
 
function OwnerInfo({ tokenId }: { tokenId: string }) {
  const { data: owner } = useOwnerOf(tokenId);
  const { data: balance } = useBalanceOf(owner ?? undefined);
 
  return <p>Owner {owner} has {balance?.toString()} tokens</p>;
}

Batch RPC Hooks

function Scores({ tokenIds, gameAddress }: Props) {
  const { data: scores } = useScoreBatch(tokenIds, gameAddress);
  const { data: gameOvers } = useGameOverBatch(tokenIds, gameAddress);
 
  return tokenIds.map((id, i) => (
    <div key={id}>
      Score: {scores?.[i]?.toString() ?? "..."} |
      Over: {gameOvers?.[i] ? "Yes" : "No"}
    </div>
  ));
}

Settings & Objectives Hooks

function GameSettings({ gameAddress }: { gameAddress: string }) {
  const { data: count } = useSettingsCount(gameAddress);
  const { data: settings } = useSettings(gameAddress);
 
  return (
    <div>
      <p>{count} settings available</p>
      {settings?.data.map(s => (
        <div key={s.id}>{s.name}: {s.description}</div>
      ))}
    </div>
  );
}

Settings, Objectives & Activity

import {
  useSettings,
  useObjectives,
  useActivity,
} from "@provable-games/denshokan-sdk/react";
 
function GameConfig({ gameAddress }: { gameAddress: string }) {
  const { data: settings } = useSettings(gameAddress);
  const { data: objectives } = useObjectives(gameAddress);
  const { data: activity } = useActivity({ gameAddress, limit: 20 });
 
  return (
    <div>
      <p>{settings?.data.length} settings</p>
      <p>{objectives?.data.length} objectives</p>
      <p>{activity?.data.length} recent actions</p>
    </div>
  );
}

Subscription Hooks

Type-safe real-time event subscriptions. Each hook subscribes to a specific WebSocket channel and returns typed events with buffering.

import {
  useScoreUpdates,
  useGameOverEvents,
  useMintEvents,
  useTokenUpdates,
  useNewGames,
  useNewMinters,
  useNewSettings,
  useNewObjectives,
  useConnectionStatus,
} from "@provable-games/denshokan-sdk/react";
 
function LiveDashboard({ gameId }: { gameId: number }) {
  const { lastEvent, events, isConnected, clear } = useScoreUpdates({
    gameIds: [gameId],
    bufferSize: 100,
    onEvent: (event) => console.log("Score:", event.score),
  });
 
  const { isConnected: wsConnected } = useConnectionStatus();
 
  return (
    <div>
      <p>{wsConnected ? "Connected" : "Reconnecting..."}</p>
      <p>Latest: {lastEvent?.playerName}{lastEvent?.score}</p>
      <button onClick={clear}>Clear History</button>
    </div>
  );
}
HookChannelPayload Type
useScoreUpdatesscoresScoreEvent
useGameOverEventsgame_overGameOverEvent
useMintEventsmintsMintEvent
useTokenUpdatestokensTokenUpdateEvent
useNewGamesgamesNewGameEvent
useNewMintersmintersNewMinterEvent
useNewSettingssettingsNewSettingEvent
useNewObjectivesobjectivesNewObjectiveEvent

Options and return types:

interface UseChannelOptions<C extends WSChannel> {
  gameIds?: number[];       // Server-side game ID filter
  bufferSize?: number;      // Max events in memory (default: 50)
  enabled?: boolean;        // Enable/disable subscription (default: true)
  onEvent?: (event: WSChannelPayloadMap[C]) => void;
}
 
interface UseChannelResult<C extends WSChannel> {
  lastEvent: WSChannelPayloadMap[C] | null;
  events: WSChannelPayloadMap[C][];
  isConnected: boolean;
  clear: () => void;
}

For full details on channels, event payloads, and the low-level useSubscription hook, see WebSocket Subscriptions.

Hook Return Type

All data hooks return the same shape:

interface UseAsyncResult<T> {
  data: T | null;       // The fetched data (null while loading)
  isLoading: boolean;   // True during initial load
  error: Error | null;  // Error if the fetch failed
  refetch: () => void;  // Manually trigger a re-fetch
}

useTokens and usePlayerTokens extend this with an additional field:

interface UseTokensResult extends UseAsyncResult<PaginatedResult<Token>> {
  /** True while token URIs are being fetched (when includeUri: true) */
  isLoadingUri: boolean;
}

See UseAsyncResult, UseTokensResult, and UseChannelResult for the full type definitions.

Complete Hook Signatures

Data Hooks

HookParametersReturn
useGames(params?){ limit?, offset? }UseAsyncResult<PaginatedResult<Game>>
useGame(address?)string | undefinedUseAsyncResult<Game>
useGameStats(address?)string | undefinedUseAsyncResult<GameStats>
useTokens(params?)TokensFilterParamsUseTokensResult
useToken(tokenId?)string | undefinedUseAsyncResult<Token>
useTokenScores(tokenId?, limit?)string | undefined, numberUseAsyncResult<TokenScoreEntry[]>
useDecodeToken(tokenId?)string | undefinedCoreToken | null
usePlayerStats(address?)string | undefinedUseAsyncResult<PlayerStats>
usePlayerTokens(address?, params?)string | undefined, PlayerTokensParamsUseTokensResult
useMinters(params?){ limit?, offset? }UseAsyncResult<PaginatedResult<Minter>>
useSettings(params?)SettingsParamsUseAsyncResult<PaginatedResult<GameSettingDetails>>
useObjectives(params?)ObjectivesParamsUseAsyncResult<PaginatedResult<GameObjectiveDetails>>
useActivity(params?)ActivityParamsUseAsyncResult<PaginatedResult<ActivityEvent>>

RPC Hooks

HookParametersReturn
useBalanceOf(account?)string | undefinedUseAsyncResult<bigint>
useOwnerOf(tokenId?)string | undefinedUseAsyncResult<string>
useTokenUri(tokenId?)string | undefinedUseAsyncResult<string>
useTokenUriBatch(tokenIds?)string[] | undefinedUseAsyncResult<string[]>
useTokenMetadataBatch(tokenIds?)string[] | undefinedUseAsyncResult<TokenMetadata[]>
useScoreBatch(tokenIds?, gameAddress?)string[] | undefined, string | undefinedUseAsyncResult<bigint[]>
useGameOverBatch(tokenIds?, gameAddress?)string[] | undefined, string | undefinedUseAsyncResult<boolean[]>
useObjectivesCount(gameAddress?)string | undefinedUseAsyncResult<number>
useSettingsCount(gameAddress?)string | undefinedUseAsyncResult<number>

Caching & Refetch Behavior

Hooks do not auto-poll or auto-refresh. Data is fetched:

  1. On mount — when the component first renders with defined parameters
  2. On parameter change — when hook parameters change (referential equality)

To manually refresh data, call refetch():

function TokenView({ tokenId }: { tokenId: string }) {
  const { data: token, refetch } = useToken(tokenId);
 
  return (
    <div>
      <p>Score: {token?.score}</p>
      <button onClick={refetch}>Refresh</button>
    </div>
  );
}

For live updates, combine data hooks with subscription hooks:

function LiveScore({ tokenId, gameId }: Props) {
  const { data: token, refetch } = useToken(tokenId);
 
  useScoreUpdates({
    gameIds: [gameId],
    onEvent: (event) => {
      if (event.tokenId === tokenId) {
        refetch(); // Re-fetch when this token's score changes
      }
    },
  });
 
  return <p>Score: {token?.score}</p>;
}

Conditional Fetching

Hooks accept undefined parameters and won't fetch until all required params are defined:

function TokenDetail({ tokenId }: { tokenId?: string }) {
  // Won't fetch until tokenId is defined
  const { data } = useToken(tokenId);
}

This makes it safe to chain hooks:

function GameInfo({ tokenId }: { tokenId: string }) {
  const { data: token } = useToken(tokenId);
  // Only fetches once token.gameAddress is available
  const { data: game } = useGame(token?.gameAddress);
}