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>
);
}| Hook | Channel | Payload Type |
|---|---|---|
useScoreUpdates | scores | ScoreEvent |
useGameOverEvents | game_over | GameOverEvent |
useMintEvents | mints | MintEvent |
useTokenUpdates | tokens | TokenUpdateEvent |
useNewGames | games | NewGameEvent |
useNewMinters | minters | NewMinterEvent |
useNewSettings | settings | NewSettingEvent |
useNewObjectives | objectives | NewObjectiveEvent |
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
| Hook | Parameters | Return |
|---|---|---|
useGames(params?) | { limit?, offset? } | UseAsyncResult<PaginatedResult<Game>> |
useGame(address?) | string | undefined | UseAsyncResult<Game> |
useGameStats(address?) | string | undefined | UseAsyncResult<GameStats> |
useTokens(params?) | TokensFilterParams | UseTokensResult |
useToken(tokenId?) | string | undefined | UseAsyncResult<Token> |
useTokenScores(tokenId?, limit?) | string | undefined, number | UseAsyncResult<TokenScoreEntry[]> |
useDecodeToken(tokenId?) | string | undefined | CoreToken | null |
usePlayerStats(address?) | string | undefined | UseAsyncResult<PlayerStats> |
usePlayerTokens(address?, params?) | string | undefined, PlayerTokensParams | UseTokensResult |
useMinters(params?) | { limit?, offset? } | UseAsyncResult<PaginatedResult<Minter>> |
useSettings(params?) | SettingsParams | UseAsyncResult<PaginatedResult<GameSettingDetails>> |
useObjectives(params?) | ObjectivesParams | UseAsyncResult<PaginatedResult<GameObjectiveDetails>> |
useActivity(params?) | ActivityParams | UseAsyncResult<PaginatedResult<ActivityEvent>> |
RPC Hooks
| Hook | Parameters | Return |
|---|---|---|
useBalanceOf(account?) | string | undefined | UseAsyncResult<bigint> |
useOwnerOf(tokenId?) | string | undefined | UseAsyncResult<string> |
useTokenUri(tokenId?) | string | undefined | UseAsyncResult<string> |
useTokenUriBatch(tokenIds?) | string[] | undefined | UseAsyncResult<string[]> |
useTokenMetadataBatch(tokenIds?) | string[] | undefined | UseAsyncResult<TokenMetadata[]> |
useScoreBatch(tokenIds?, gameAddress?) | string[] | undefined, string | undefined | UseAsyncResult<bigint[]> |
useGameOverBatch(tokenIds?, gameAddress?) | string[] | undefined, string | undefined | UseAsyncResult<boolean[]> |
useObjectivesCount(gameAddress?) | string | undefined | UseAsyncResult<number> |
useSettingsCount(gameAddress?) | string | undefined | UseAsyncResult<number> |
Caching & Refetch Behavior
Hooks do not auto-poll or auto-refresh. Data is fetched:
- On mount — when the component first renders with defined parameters
- 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);
}