The Dojo Game Starter uses a sophisticated hook pattern that separates concerns while maintaining seamless integration between React components and blockchain operations. Each hook has a specific responsibility, but they work together to create a unified, reactive gaming experience.
🔗 Hook Dependency Graph
┌─────────────────────────────────────────────────────────────────┐
│ COMPONENT LAYER │
├─────────────────────────────────────────────────────────────────┤
│ 📱 StatusBar │ 🎮 GameActions │ 📊 PlayerStats │
│ - Connection │ - Train/Mine │ - Experience │
│ - Player Status │ - Rest Actions │ - Health/Coins │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ HOOK ORCHESTRATION │
├─────────────────────────────────────────────────────────────────┤
│ 🔌 useStarknetConnect → 👤 usePlayer → 🎮 useSpawnPlayer │
│ ↓ ↓ │
│ 🏋️ useTrainAction ⛏️ useMineAction │
│ 💤 useRestAction 📊 useGameStats │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ INTEGRATION LAYER │
├─────────────────────────────────────────────────────────────────┤
│ 🏪 Zustand Store │ 🔗 Dojo SDK │ 📡 GraphQL Queries │
│ - Global State │ - Contracts │ - Torii Indexer │
└─────────────────────────────────────────────────────────────────┘
This hook manages the fundamental connection to Starknet via Cartridge Controller.
🔑 Core Connection Logic:
const handleConnect = useCallback(async () => {
const connector = connectors[0]; // Cartridge Controller
try {
setIsConnecting(true);
console.log("🔗 Attempting to connect controller...");
// Opens Cartridge Controller interface
await connect({ connector });
console.log("✅ Controller connected successfully");
} catch (error) {
console.error("❌ Connection failed:", error);
} finally {
setIsConnecting(false);
}
}, [connect, connectors]);📤 Return Interface:
return {
status, // 'connected' | 'disconnected' | 'connecting'
address, // Wallet address when connected
isConnecting, // Connection loading state
handleConnect, // Function to initiate connection
handleDisconnect, // Function to disconnect
};🎯 Key Responsibilities:
- Controller Integration: Direct interface with Cartridge Controller
- Connection State: Comprehensive connection status management
- Error Handling: Robust error management for connection failures
- Auto-Reconnection: Supports automatic reconnection on page refresh
The data backbone of the game, connecting GraphQL queries to Zustand state.
🔍 GraphQL Query Structure:
const PLAYER_QUERY = `
query GetPlayer($playerOwner: ContractAddress!) {
fullStarterReactPlayerModels(where: { owner: $playerOwner }) {
edges {
node {
owner
experience
health
coins
creation_day
}
}
}
}
`;🔄 Data Fetching Logic:
const fetchPlayerData = async (playerOwner: string): Promise<Player | null> => {
const response = await fetch(TORII_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: PLAYER_QUERY,
variables: { playerOwner }
}),
});
const result = await response.json();
if (!result.data?.fullStarterReactPlayerModels?.edges?.length) {
return null; // Player not found
}
// Convert hex blockchain values to JavaScript numbers
const rawData = result.data.fullStarterReactPlayerModels.edges[0].node;
return {
owner: rawData.owner,
experience: hexToNumber(rawData.experience),
health: hexToNumber(rawData.health),
coins: hexToNumber(rawData.coins),
creation_day: hexToNumber(rawData.creation_day)
};
};🏪 Zustand Integration:
// Get player from store and setter function
const storePlayer = useAppStore(state => state.player);
const setPlayer = useAppStore(state => state.setPlayer);
// Auto-fetch when wallet address changes
useEffect(() => {
if (userAddress) {
fetchPlayerData(userAddress).then(setPlayer);
}
}, [userAddress]);🎯 Key Features:
- GraphQL Integration: Direct queries to Torii indexer
- Data Transformation: Converts hex blockchain values to JavaScript numbers
- Zustand Integration: Seamless state management
- Caching: Leverages store for performance
- Auto-Refresh: Reacts to wallet address changes
The most complex hook, handling player creation and initialization logic.
🛡️ Race Condition Prevention:
const [isInitializing, setIsInitializing] = useState(false);
const initializePlayer = useCallback(async () => {
// Prevent multiple executions
if (isInitializing) {
return { success: false, error: "Already initializing" };
}
setIsInitializing(true);
// ... rest of logic
}, [isInitializing]);✅ Validation Chain:
// Multi-step validation before creating player
if (status !== "connected") {
return { success: false, error: "Controller not connected" };
}
if (!account) {
return { success: false, error: "No account found" };
}🔍 Player Existence Check:
// Check if player already exists
console.log("🔄 Checking for existing player...");
setInitState(prev => ({ ...prev, step: 'checking' }));
await refetchPlayer(); // Use usePlayer hook to refresh data
if (player) {
// Player exists - no need to create
return { success: true, playerExists: true };
}🎮 Player Creation Flow:
// Create new player via Dojo SDK
console.log("🎮 Creating new player...");
setInitState(prev => ({ ...prev, step: 'spawning', txStatus: 'PENDING' }));
const txResult = await client.game.spawnPlayer(account);
if (txResult && txResult.code === "SUCCESS") {
// Refresh player data after creation
await refetchPlayer();
return {
success: true,
playerExists: false,
transactionHash: txResult.transaction_hash
};
}📊 Complex State Tracking:
interface InitializeState {
isInitializing: boolean;
error: string | null;
step: 'checking' | 'spawning' | 'loading' | 'success';
txHash: string | null;
txStatus: 'PENDING' | 'SUCCESS' | 'REJECTED' | null;
}🎯 Complex State Management:
- Multi-step Process: Checking → Spawning → Loading → Success
- Transaction Tracking: Complete transaction lifecycle
- Error Recovery: Comprehensive error handling
- Race Condition Prevention: Multiple execution guards
- Hook Integration: Coordinates with
usePlayeranduseStarknetConnect
Each game action follows the same optimistic pattern but with action-specific logic.
🎯 Action Validation:
const { account, status } = useAccount();
const { player } = useAppStore();
const isConnected = status === "connected";
const hasPlayer = player !== null;
const canTrain = isConnected && hasPlayer && !trainState.isLoading;⚡ Optimistic Update Pattern:
const executeTrain = useCallback(async () => {
try {
// 1. ⚡ IMMEDIATE UI UPDATE
setTrainState({ isLoading: true, txStatus: 'PENDING', ... });
updatePlayerExperience((player?.experience || 0) + 10);
// 2. 🔗 BLOCKCHAIN TRANSACTION
const tx = await client.game.train(account);
// 3. ✅ CONFIRMATION
if (tx && tx.code === "SUCCESS") {
setTrainState({ txStatus: 'SUCCESS', isLoading: false });
}
} catch (error) {
// 4. ❌ ROLLBACK on failure
updatePlayerExperience((player?.experience || 0) - 10);
setTrainState({ error: error.message, txStatus: 'REJECTED' });
}
}, [client, account, player]);🔄 Auto-Cleanup Logic:
// Auto-clear success state after 3 seconds
setTimeout(() => {
setTrainState({
isLoading: false,
error: null,
txHash: null,
txStatus: null
});
}, 3000);Each action hook follows the same pattern but with different validation and effects:
⛏️ useMineAction - Health Validation:
const hasEnoughHealth = (player?.health || 0) > 5;
const canMine = isConnected && hasPlayer && hasEnoughHealth && !mineState.isLoading;
// Optimistic update: +5 coins, -5 health
updatePlayerCoins((player?.coins || 0) + 5);
updatePlayerHealth(Math.max(0, (player?.health || 100) - 5));💤 useRestAction - Full Health Check:
const needsHealth = (player?.health || 0) < 100;
const canRest = isConnected && hasPlayer && needsHealth && !restState.isLoading;
// Optimistic update: +20 health (max 100)
updatePlayerHealth(Math.min(100, (player?.health || 100) + 20));// GameActions.tsx - Component using multiple action hooks
export function GameActions() {
const player = useAppStore(state => state.player);
// Each action has its own dedicated hook
const { trainState, executeTrain, canTrain } = useTrainAction();
const { mineState, executeMine, canMine } = useMineAction();
const { restState, executeRest, canRest } = useRestAction();
const actions = [
{
icon: Dumbbell,
label: "Train",
description: "+10 EXP",
onClick: executeTrain,
state: trainState,
canExecute: canTrain,
color: "from-blue-500 to-blue-600",
},
{
icon: Hammer,
label: "Mine",
description: "+5 Coins, -5 Health",
onClick: executeMine,
state: mineState,
canExecute: canMine,
color: "from-yellow-500 to-yellow-600",
disabledReason: !canMine && player && (player.health || 0) <= 5
? "Low Health!"
: undefined,
},
{
icon: Bed,
label: "Rest",
description: "+20 Health",
onClick: executeRest,
state: restState,
canExecute: canRest,
color: "from-green-500 to-green-600",
disabledReason: !canRest && player && (player.health || 0) >= 100
? "Full Health!"
: undefined,
},
];
return (
<div className="space-y-4">
{actions.map((action) => {
const Icon = action.icon;
const isLoading = action.state.isLoading;
return (
<Button
key={action.label}
onClick={action.onClick}
disabled={!action.canExecute || isLoading}
className={`w-full h-14 bg-gradient-to-r ${action.color} hover:scale-105 transition-all duration-300`}
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{action.label}ing...
</>
) : (
<>
<Icon className="w-5 h-5 mr-2" />
{action.label} ({action.description})
</>
)}
</Button>
);
})}
</div>
);
}// StatusBar.tsx - Orchestrating multiple hooks
export function StatusBar() {
const { status, address, handleConnect, handleDisconnect } = useStarknetConnect();
const { player, isLoading: playerLoading } = usePlayer();
const { initializePlayer, isInitializing, txStatus } = useSpawnPlayer();
const { connector } = useAccount();
const isConnected = status === "connected";
const isLoading = isConnecting || isInitializing || playerLoading;
// 🎮 Auto-initialize player after connection
useEffect(() => {
if (isConnected && !player && !isInitializing && !playerLoading) {
console.log("🎮 Controller connected, auto-initializing player...");
setTimeout(() => {
initializePlayer().then(result => {
console.log("🎮 Auto-initialization result:", result);
});
}, 500);
}
}, [isConnected, player, isInitializing, playerLoading, initializePlayer]);
// Status message logic
const getStatusMessage = () => {
if (!isConnected) return "Connect your controller to start playing";
if (playerLoading) return "Loading player data...";
if (isInitializing) {
if (txStatus === 'PENDING') return "Creating player on blockchain...";
if (txStatus === 'SUCCESS') return "Player created successfully!";
return "Initializing player...";
}
if (player) return "Ready to play!";
return "Preparing...";
};
return (
<div className="status-bar">
{/* Connection UI */}
{!isConnected ? (
<Button onClick={handleConnect} disabled={isLoading}>
{isConnecting ? "Connecting..." : "Connect Controller"}
</Button>
) : (
<div className="connected-state">
<span>Connected: {formatAddress(address)}</span>
<span>{getStatusMessage()}</span>
</div>
)}
</div>
);
}1. useStarknetConnect establishes wallet connection
↓
2. usePlayer automatically fetches player data when address changes
↓
3. useSpawnPlayer uses player data to determine if creation is needed
↓
4. Game action hooks (useTrainAction, etc.) use player data for validation
↓
5. All hooks update Zustand store for reactive UI updates
The React hooks pattern in Dojo Game Starter provides a clean, reusable, and maintainable way to manage complex blockchain interactions while maintaining excellent user experience through optimistic updates and comprehensive error handling.
Next: We'll explore the complete Data Flow to understand how all these pieces work together in real-time gameplay scenarios.