Sui dApp Kit Frontend
dApp Kit provides a React starter template that provides scaffolding for wallet connection, transaction building, onchain queries, and cache invalidation. In this example, the app connects to a deployed Hero NFT contract, mints an NFT with a single button click, and displays all owned heroes by filtering objects by type. This example does not include Move code, as it focuses entirely on the TypeScript frontend patterns.
When to use this pattern
Use this pattern when you need to:
-
Set up a Sui-connected React app from scratch with wallet management and network switching.
-
Build and execute transactions from a browser using
signAndExecuteTransaction. -
Query a user's owned objects filtered by a specific Move type (for example, all
HeroNFTs). -
Automatically refresh the UI after a transaction completes using React Query cache invalidation.
-
Create a starter template for any Sui app frontend that you can extend with your own contract calls.
What you learn
This example teaches:
-
Provider setup: The app wraps components in
QueryClientProvider(React Query) andDAppKitProvider(Sui). The dApp Kit config createsSuiGrpcClientinstances for each network. This is the foundation every Sui React app needs. -
Wallet hooks:
useCurrentAccount()returns the connected wallet address (or null).useDAppKit()providessignAndExecuteTransactionand the Sui client. These hooks drive all wallet-dependent UI. -
Transaction building: The
Transactionclass creates a PTB.tx.moveCall()calls a Move function.tx.transferObjects()sends the result to the user.signAndExecuteTransactionprompts the wallet to sign and submit. -
Reactive queries:
useQueryfetches data from the chain with a query key tied to the wallet address. When the key changes (wallet connects/disconnects) or the cache is invalidated (after a transaction), React Query refetches automatically. -
Cache invalidation pattern: After
waitForTransactionconfirms a transaction,queryClient.invalidateQueriesmarks the owned-objects cache as stale. React Query refetches in the background, and the component re-renders with the new data.
Architecture
The example has 1 React app that connects to a deployed Move contract on Testnet. The React app renders a mint button, a wallet status card, and an owned-objects list. The wallet (Slush Wallet or any Sui-compatible wallet) handles signing. The Sui gRPC node serves object queries for the owned-objects list.
The diagram below traces 1 mint-and-refresh cycle.
The following steps walk through the flow:
-
The user clicks Mint NFT. The frontend builds a PTB with
moveCalltargeting the deployedmint_herofunction andtransferObjectsto send the result to the user's address. -
The frontend calls
signAndExecuteTransaction, which prompts the wallet to sign and submit. The wallet executes the transaction on Testnet. -
The frontend calls
waitForTransactionto wait for confirmation, then callsqueryClient.invalidateQuerieswith the["ownedObjects", address]key. -
React Query detects the stale cache and refetches by calling
listOwnedObjectswith aStructTypefilter forHero. The component re-renders with the updated list.
Prerequisites
- Prerequisites
-
Download and install an IDE. The following are recommended, as they offer Move extensions:
-
VSCode, corresponding Move extension
-
Emacs, corresponding Move extension
-
Vim, corresponding Move extension
-
Zed, corresponding Move extension
Alternatively, you can use the Move web IDE, which does not require a download. It does not support all functions necessary for this guide, however.
-
-
Node.js 18 or later
-
A Sui wallet (Slush Wallet or another compatible wallet)
Setup
Follow these steps to set up the example locally.
Step 1: Clone the repo
$ git clone -b solution https://github.com/MystenLabs/sui-move-bootcamp.git
$ cd sui-move-bootcamp/E2/my-first-sui-dapp
Step 2: Install dependencies
$ pnpm install
Step 3: Start the dev server
$ pnpm dev
You do not need a .env file if you use the hardcoded contract addresses in the source.
Run the example
Open http://localhost:5173 in a browser and click Connect in the top-right corner to link your wallet. Once connected, the wallet status card shows your address. Click Mint NFT to mint a Hero. After the transaction confirms, the owned-objects section updates to show your new hero's object ID.
Key code highlights
The following snippets are the parts of the code worth reading carefully.
dApp Kit configuration
The dapp-kit.ts file configures multi-network gRPC clients for Devnet, Testnet, and Mainnet.
E2/my-first-sui-dapp/src/dapp-kit.ts. You probably need to run `pnpm prebuild` and restart the site.The createDAppKit factory takes an array of network configs, each with a chain identifier and a createClient factory. The SuiGrpcClient uses binary protocol for faster communication than JSON-RPC. The module declaration augments dApp Kit's types so TypeScript knows about the configured networks.
Minting an NFT with a PTB
The MintNFTForm component builds a transaction, signs it with the wallet, and invalidates the cache after confirmation.
E2/my-first-sui-dapp/src/components/ui/MintNFTForm.tsx. You probably need to run `pnpm prebuild` and restart the site.The handleMint function builds a Transaction with moveCall targeting the deployed mint_hero function, then transferObjects to send the hero to the user's address. After signAndExecuteTransaction returns the digest, it waits for confirmation and invalidates the ["ownedObjects", address] query key so the heroes list refetches.
Querying owned objects by type
The OwnedObjects component uses listOwnedObjects with a StructType filter to show only Hero NFTs.
E2/my-first-sui-dapp/src/OwnedObjects.tsx. You probably need to run `pnpm prebuild` and restart the site.The useQuery hook fetches owned objects only when a wallet is connected (enabled: !!account). The StructType filter limits results to the Hero type. The query key includes the address so the cache is per-wallet. React Query handles loading, error, and empty states.
Provider hierarchy
The main.tsx entry point wraps the app in the required providers.
E2/my-first-sui-dapp/src/main.tsx. You probably need to run `pnpm prebuild` and restart the site.The provider order matters: QueryClientProvider must wrap DAppKitProvider because dApp Kit hooks use React Query internally. The initialChain prop sets Testnet as the default network.
Common modifications
-
Add network switching: Use dApp Kit's
useChainSwitchhook to let users switch between Testnet and Mainnet from the UI. The gRPC clients are already configured for all 3 networks. -
Display object details: After listing owned heroes, call
getObjectwithshowContent: trueto fetch each hero's name, stamina, and weapon. Render hero cards instead of raw IDs. -
Add form inputs for mint parameters: Replace the hardcoded mint arguments with a form that accepts hero name and stamina. Pass user values to
tx.pure.string()andtx.pure.u64(). -
Add pagination for owned objects: The
listOwnedObjectsresponse includes anextCursor. Use React Query'suseInfiniteQueryto load more objects on scroll. -
Add transaction history: After each mint, store the transaction digest in local state or
localStorage. Display a history section with links to the Sui explorer.
Troubleshooting
The following sections address common issues with this example.
Wallet does not appear in connect modal
Symptom: Clicking Connect opens the modal but no wallet is listed.
Cause: No supported wallet extension is installed, or the extension is disabled for the current site.
Fix: Install Slush Wallet or another Sui-compatible wallet. Enable it for localhost:5173. Refresh the page.
Owned objects list is empty after minting
Symptom: The mint transaction succeeds but no heroes appear in the list.
Cause: The StructType filter in OwnedObjects.tsx does not match the deployed contract's full type path, or the component did not invalidate the React Query cache.
Fix: Verify the StructType matches PACKAGE_ID::hero::Hero exactly. Confirm invalidateQueries uses the same query key as the useQuery call.
signAndExecuteTransaction fails silently
Symptom: Clicking Mint NFT does nothing and no wallet popup appears.
Cause: The wallet is not connected, or the useDAppKit() hook is called outside the DAppKitProvider.
Fix: Check that useCurrentAccount() returns a non-null value before calling signAndExecuteTransaction. Verify DAppKitProvider wraps the component tree in main.tsx.
gRPC client returns network error
Symptom: Queries fail with a connection or timeout error.
Cause: The gRPC endpoint URL is wrong or unreachable from the browser.
Fix: Verify the URLs in dapp-kit.ts match the current Sui gRPC endpoints. The standard endpoints are https://grpc.testnet.sui.io for Testnet and https://grpc.mainnet.sui.io for Mainnet.