Skip to content

React Client

Run the dev server

From the root of the project, run:

Easy method:

./scripts/client.sh

Manual method:

pnpm i && pnpm dev

The client requires environment variables to be set. See the Environment Variables page for more information.

Project Structure

Elements

  • Small reusable react UI components

Components

  • Stateful react components composed of Elements. This is where all the onchain state should be stored.

Modules

  • Collection of components composed into Modules

Containers

  • Locations where Modules are composed into a full page

Layouts

  • Collection of containers composed into a full layout

useDojo Hook

The useDojo hook is a core part of the Dojo framework that provides access to essential game functionality, account management, and network interactions.

Usage

import { useDojo } from "@/hooks/context/DojoContext";
function GameComponent() {
  const { account, network, masterAccount } = useDojo();
  // ... your game logic
}

Features

Account Management

The hook provides comprehensive wallet and account management capabilities:

  • Burner Wallet Creation: Create temporary wallets for players
  • Account Selection: Switch between different burner wallets
  • Account Listing: View all available burner wallets
  • Master Account Access: Access to the game's master account
  • Deployment Status: Track wallet deployment status

Network Integration

Access to network configuration and setup results for interacting with the Starknet blockchain.

Game Setup

Provides access to all initialized game systems, components, and world configurations.

Requirements

The hook must be used within a DojoProvider component and requires the following environment variables:

  • VITE_PUBLIC_MASTER_ADDRESS
  • VITE_PUBLIC_MASTER_PRIVATE_KEY
  • VITE_PUBLIC_ACCOUNT_CLASS_HASH

Return Value

The hook returns an object containing:

  • setup: Complete Dojo context including game systems and components
  • account: Account management functions and state
  • network: Network configuration and setup results
  • masterAccount: The master account instance

Example

function GameUI() {
  const { account } = useDojo();
  return (
    <div>
      <p>Current Account: {account.account.address}</p>
    </div>
  );
}

Eternum provider

Wrapper around the DojoProvider, which itself is a wrapper around a generic Starknet provider, an API to easy interact with your contract’s API via the RPC. Each layer of abstraction adds its own set of functionalities, with the Eternum Provider mainly providing easy access to all the system calls that our systems expose (create_army, etc…)

Offchain messages

You can use offchain messages to store information in the indexer, but it is not persisted onchain. Some examples of use cases for this are in-game messages. Refer to this code for an example of how to use offchain messages

State Management with Recs

Recs allow you to query the state of the world and subscribe to changes in the world. It is the recommended way to manage state in the client.

const structureAtPosition = runQuery([HasValue(Position, { x, y }), Has(Structure)]);

This line of code will run a query against the local state of the browser and return all the structures that are at the position { x, y }. This does not execute any call to Torii as the local state is already synced with the latest updates.

This part of the code in client/src/dojo/setup.ts is where the local state is initialized and synced.

Subscribing to changes

To subscribe to changes in the world, you can use the useEntityQuery hook. This hook will return the entity that matches the query and update it if it changes.

const allRealms = useEntityQuery([Has(Realm)]);

Getting the value of your components

After you have run a query, you can get the value of your components using the getComponentValue function.

const realm = getComponentValue(Realm, entityId);

The entityId is the poseidon hash of all the keys of the component you want to get. This can be an easier way to get your component than using a query if you already know all the keys.

Getting a component from an entity

If you have an entityId, you can get a component from it using the getComponent function.

const realm = getComponentValue(Realm, getEntityIdFromKeys([entityId]));

The previous line of code is equivalent to:

const entity = runQuery([Has(Realm), HasValue(Keys, [entityId])]);
const realm = getComponentValue(Realm, Array.from(entity)[0]);

Extending the client

Adding a new component

You will need to add your component to the contractComponents.ts file. This will ensure that the component is synced with the state of the world and will provide the types for the component.

Adding a new system

You will need to add your system logic to the sdk/packages/eternum/src/provider/index.ts file and then use it in the createSystemCalls.ts file. This will ensure that the system is called with the correct arguments and will provide the types for the system.

Optimistic updates

You can use optimistic updates to update the local state without waiting for the transaction to be included in a block. This is useful to provide a better user experience by updating the UI immediately.

private _optimisticDestroy = (entityId: ID, col: number, row: number) => {
	const overrideId = uuid();
	const realmPosition = getComponentValue(this.setup.components.Position, getEntityIdFromKeys([BigInt(entityId)]));
	const { x: outercol, y: outerrow } = realmPosition || { x: 0, y: 0 };
	const entity = getEntityIdFromKeys([outercol, outerrow, col, row].map((v) => BigInt(v)));
	this.setup.components.Building.addOverride(overrideId, {
		entity,
		value: {
		outer_col: outercol,
		outer_row: outerrow,
		inner_col: col,
		inner_row: row,
		category: "None",
		produced_resource_type: 0,
		bonus_percent: 0,
		entity_id: 0,
		outer_entity_id: 0,
		},
	});
	return overrideId;
};

You can use the uuid function to generate a unique overrideId. Remember to return the overrideId so you can later delete it after the transaction is included in a block. It's good practice to remove the override, however because of a delay between the transaction being included in a block and Torii syncing, you might have a split second where the override is removed and the Recs being updated. This will cause your component (i.e. a building) to appear then disappear for a second before reappearing.