| name | sui-frontend |
| description | Sui frontend dApp development with @mysten/dapp-kit-react (React) and @mysten/dapp-kit-core (Vue, vanilla JS, other frameworks). Use when building browser apps that connect to Sui wallets, query on-chain data, or execute transactions. Use alongside the sui-ts-sdk skill for PTB construction patterns. |
Sui Frontend Skill
This skill covers building browser-based Sui dApps using the dApp Kit SDK. The SDK has two packages:
@mysten/dapp-kit-react โ React hooks, DAppKitProvider, and React component wrappers
@mysten/dapp-kit-core โ Framework-agnostic core: actions, nanostores state, and Web Components for Vue, vanilla JS, Svelte, or any other framework
Both packages expose the same createDAppKit factory and identical action APIs (signAndExecuteTransaction, signTransaction, signPersonalMessage, etc.). What differs is how you access reactive state and render UI: React uses hooks and provider components; other frameworks use nanostores stores and Web Components.
For PTB construction details (splitCoins, moveCall, coinWithBalance, etc.), apply the sui-ts-sdk skill alongside this one โ the Transaction API is identical in browser and Node contexts.
Note: The older @mysten/dapp-kit package is deprecated (JSON-RPC only, no gRPC/GraphQL support). New projects must use @mysten/dapp-kit-react or @mysten/dapp-kit-core.
1. Package Installation
React:
npm install @mysten/dapp-kit-react @mysten/sui
npm install @tanstack/react-query
Vue / vanilla JS / other frameworks:
npm install @mysten/dapp-kit-core @mysten/sui
npm install @nanostores/vue
| Package | Purpose |
|---|
@mysten/dapp-kit-react | React hooks, DAppKitProvider, React component wrappers |
@mysten/dapp-kit-core | Framework-agnostic actions, stores, Web Components |
@mysten/sui | Sui TypeScript SDK (Transaction class, gRPC client) |
@tanstack/react-query | Declarative on-chain data fetching (React only) |
@nanostores/vue | Reactive store bindings for Vue |
2. Instance & Provider Setup (React)
Not using React? Skip to the Non-React Integration section below.
The new dApp Kit uses a single createDAppKit factory instead of three nested providers. Create the instance once in a dedicated file, then wrap your app with DAppKitProvider:
import { createDAppKit } from '@mysten/dapp-kit-react';
import { SuiGrpcClient } from '@mysten/sui/grpc';
const GRPC_URLS: Record<string, string> = {
testnet: 'https://fullnode.testnet.sui.io:443',
mainnet: 'https://fullnode.mainnet.sui.io:443',
};
export const dAppKit = createDAppKit({
networks: ['testnet', 'mainnet'],
defaultNetwork: 'testnet',
createClient: (network) =>
new SuiGrpcClient({ network, baseUrl: GRPC_URLS[network] }),
});
declare module '@mysten/dapp-kit-react' {
interface Register {
dAppKit: typeof dAppKit;
}
}
import { DAppKitProvider, ConnectButton } from '@mysten/dapp-kit-react';
import { dAppKit } from './dapp-kit';
export default function App() {
return (
<DAppKitProvider dAppKit={dAppKit}>
<ConnectButton />
<YourApp />
</DAppKitProvider>
);
}
The declare module augmentation is what makes useDAppKit() and other hooks return properly typed values without passing the instance explicitly. It's the recommended approach โ the alternative is passing dAppKit directly to each hook call (e.g. useWalletConnection({ dAppKit })), but that's more verbose.
3. Network & Client Configuration
createDAppKit accepts these key options:
createDAppKit({
networks: ['testnet', 'mainnet'],
defaultNetwork: 'testnet',
createClient: (network) =>
new SuiGrpcClient({ network, baseUrl: GRPC_URLS[network] }),
autoConnect: true,
});
Use SuiGrpcClient here โ unlike the deprecated @mysten/dapp-kit, the new package is built for gRPC. Do not pass SuiJsonRpcClient to createClient.
import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc';
createClient: (network) => new SuiJsonRpcClient({ ... })
import { SuiGrpcClient } from '@mysten/sui/grpc';
createClient: (network) => new SuiGrpcClient({ network, baseUrl: GRPC_URLS[network] })
Non-React Integration (Vue / Vanilla JS / Svelte)
Use @mysten/dapp-kit-core when not building with React. The createDAppKit call is identical โ only the import path differs:
import { createDAppKit } from '@mysten/dapp-kit-core';
import { SuiGrpcClient } from '@mysten/sui/grpc';
const GRPC_URLS: Record<string, string> = {
testnet: 'https://fullnode.testnet.sui.io:443',
mainnet: 'https://fullnode.mainnet.sui.io:443',
};
export const dAppKit = createDAppKit({
networks: ['testnet', 'mainnet'],
defaultNetwork: 'testnet',
createClient: (network) => new SuiGrpcClient({ network, baseUrl: GRPC_URLS[network] }),
});
No declare module augmentation needed โ that's React-only.
All actions on the instance work identically to the React sections below: signAndExecuteTransaction, signTransaction, signPersonalMessage, connectWallet, disconnectWallet, switchNetwork, switchAccount.
Web Components
Register the web components once at your app entry point, then use them in any HTML or template:
import '@mysten/dapp-kit-core/web';
Connect Button โ set instance as a DOM property (not an HTML attribute):
<mysten-dapp-kit-connect-button></mysten-dapp-kit-connect-button>
<script type="module">
import { dAppKit } from './dapp-kit.js';
document.querySelector('mysten-dapp-kit-connect-button').instance = dAppKit;
</script>
In Vue templates use property binding:
<mysten-dapp-kit-connect-button :instance="dAppKit" />
Supports modalOptions.filterFn / modalOptions.sortFn, same as the React ConnectButton.
Connect Modal โ for custom triggers (menu items, keyboard shortcuts, programmatic open):
<mysten-dapp-kit-connect-modal></mysten-dapp-kit-connect-modal>
<script type="module">
const modal = document.querySelector('mysten-dapp-kit-connect-modal');
modal.instance = dAppKit;
document.getElementById('open-btn').addEventListener('click', () => modal.show());
</script>
Modal events: open, opened, close, closed, cancel.
Reactive State (nanostores)
State is exposed as nanostores stores on dAppKit.stores:
| Store | Type | Description |
|---|
$connection | { wallet, account, status, isConnected, isConnecting, isReconnecting, isDisconnected } | Full connection state |
$currentNetwork | string | Active network name |
$currentClient | SuiClient | Client for the active network |
$wallets | UiWallet[] | Detected wallets |
Vanilla JS โ subscribe for reactive updates:
const connection = dAppKit.stores.$connection.get();
const unsubscribe = dAppKit.stores.$connection.subscribe((conn) => {
const el = document.getElementById('status');
if (!el) return;
if (conn.isConnected && conn.account) {
el.textContent = `${conn.wallet?.name}: ${conn.account.address}`;
} else {
el.textContent = 'Not connected';
}
});
unsubscribe();
Vue โ use @nanostores/vue for reactive template bindings:
<script setup lang="ts">
import { useStore } from '@nanostores/vue';
import { Transaction } from '@mysten/sui/transactions';
import { dAppKit } from './dapp-kit';
const connection = useStore(dAppKit.stores.$connection);
const network = useStore(dAppKit.stores.$currentNetwork);
async function handleTransfer() {
if (!connection.value.account) return;
const tx = new Transaction();
// ... build PTB ...
const result = await dAppKit.signAndExecuteTransaction({ transaction: tx });
if (result.FailedTransaction) {
throw new Error(result.FailedTransaction.status.error?.message ?? 'Transaction failed');
}
console.log('Digest:', result.Transaction.digest);
}
</script>
<template>
<mysten-dapp-kit-connect-button :instance="dAppKit" />
<div v-if="connection.account">
<p>Wallet: {{ connection.wallet?.name }}</p>
<p>Address: {{ connection.account.address }}</p>
<p>Network: {{ network }}</p>
<button @click="handleTransfer">Send Transaction</button>
</div>
<p v-else>Connect your wallet to get started</p>
</template>
For Svelte, Solid, and other frameworks, nanostores has dedicated integrations โ see the nanostores docs.
On-chain queries (non-React)
Outside React there's no useCurrentClient hook. Use the store or getClient() directly:
const client = dAppKit.stores.$currentClient.get();
const client = dAppKit.getClient();
const mainnetClient = dAppKit.getClient('mainnet');
const connection = dAppKit.stores.$connection.get();
if (!connection.account) throw new Error('Wallet not connected');
const balance = await client.getBalance({
owner: connection.account.address,
coinType: '0x2::sui::SUI',
});
For live queries, subscribe to $connection and $currentNetwork and re-run when they change.
4. Wallet Connection
ConnectButton
The simplest approach โ renders a "Connect Wallet" button that opens a wallet selection modal:
import { ConnectButton } from '@mysten/dapp-kit-react';
function Header() {
return (
<header>
<ConnectButton />
</header>
);
}
ConnectButton auto-connects on page load by default (controlled by autoConnect in createDAppKit). Wallet detection happens in the browser โ the component must be client-side rendered.
You can filter or sort the wallet list:
<ConnectButton
modalOptions={{
filterFn: (wallet) => wallet.name !== 'ExcludedWallet',
sortFn: (a, b) => a.name.localeCompare(b.name),
}}
/>
Supported wallets
dApp Kit auto-detects any wallet implementing the Sui Wallet Standard. No manual registration needed โ installed wallets appear automatically.
Custom connection UI
Use useWallets to list wallets and useDAppKit for the connect/disconnect actions:
import { useWallets, useDAppKit } from '@mysten/dapp-kit-react';
function WalletMenu() {
const wallets = useWallets();
const dAppKit = useDAppKit();
return (
<div>
{wallets.map((wallet) => (
<button
key={wallet.name}
onClick={() => dAppKit.connectWallet({ wallet })}
>
{wallet.name}
</button>
))}
<button onClick={() => dAppKit.disconnectWallet()}>Disconnect</button>
</div>
);
}
Connection status
useWalletConnection provides the full connection state:
import { useWalletConnection } from '@mysten/dapp-kit-react';
function ConnectionStatus() {
const { status, wallet, account } = useWalletConnection();
if (status === 'connecting' || status === 'reconnecting') return <p>Connecting...</p>;
if (status === 'connected') return <p>Connected: {wallet?.name}</p>;
return <p>Disconnected</p>;
}
5. Current Account & Wallet
useCurrentAccount gives you the connected address; useCurrentWallet gives you the wallet object (name, icon, accounts list):
import { useCurrentAccount, useCurrentWallet } from '@mysten/dapp-kit-react';
function Profile() {
const account = useCurrentAccount();
const wallet = useCurrentWallet();
if (!account) {
return <p>No wallet connected</p>;
}
return (
<div>
<p>Wallet: {wallet?.name}</p>
<p>Address: {account.address}</p>
<p>Label: {account.label}</p>
</div>
);
}
Both return null when no wallet is connected. Always null-check before accessing their properties โ TypeScript enforces this.
useCurrentAccount() โ UiWalletAccount | null โ provides address, label
useCurrentWallet() โ UiWallet | null โ provides name, icon, accounts
6. Accessing the Raw Client
useCurrentClient returns the SuiClient for the active network. Use it for imperative async calls โ inside event handlers or after a transaction:
import { useCurrentClient } from '@mysten/dapp-kit-react';
function SomeComponent() {
const client = useCurrentClient();
const handleSuccess = async (digest: string) => {
await client.waitForTransaction({ digest });
};
}
Do not instantiate new SuiGrpcClient(...) inside components โ use useCurrentClient so it stays in sync with the active network.
7. Querying On-Chain Data
useSuiClientQuery no longer exists in the new package. Use useCurrentClient with @tanstack/react-query directly:
import { useCurrentClient, useCurrentAccount } from '@mysten/dapp-kit-react';
import { useQuery } from '@tanstack/react-query';
function Balance() {
const client = useCurrentClient();
const account = useCurrentAccount();
const { data, isPending, error } = useQuery({
queryKey: ['getBalance', account?.address],
queryFn: () =>
client.getBalance({
owner: account!.address,
coinType: '0x2::sui::SUI',
}),
enabled: !!account,
});
if (isPending) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
const sui = Number(data.totalBalance) / 1_000_000_000;
return <p>Balance: {sui.toFixed(4)} SUI</p>;
}
Always wrap @tanstack/react-query usage in a QueryClientProvider:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
<QueryClientProvider client={queryClient}>
<DAppKitProvider dAppKit={dAppKit}>
<App />
</DAppKitProvider>
</QueryClientProvider>
Always pass enabled: !!account for queries that require a connected wallet. Without it, the query fires immediately with an undefined owner and errors.
8. Paginated Queries
Use useInfiniteQuery from @tanstack/react-query paired with useCurrentClient:
import { useCurrentClient, useCurrentAccount } from '@mysten/dapp-kit-react';
import { useInfiniteQuery } from '@tanstack/react-query';
function OwnedNFTs() {
const client = useCurrentClient();
const account = useCurrentAccount();
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ['getOwnedObjects', account?.address],
queryFn: ({ pageParam }) =>
client.getOwnedObjects({
owner: account!.address,
cursor: pageParam ?? null,
filter: { StructType: '0xPKG::nft::NFT' },
options: { showContent: true },
}),
initialPageParam: undefined,
getNextPageParam: (lastPage) =>
lastPage.hasNextPage ? lastPage.nextCursor : undefined,
enabled: !!account,
});
const allObjects = data?.pages.flatMap((page) => page.data) ?? [];
return (
<div>
{allObjects.map((obj) => (
<NFTCard key={obj.data?.objectId} object={obj} />
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load more'}
</button>
)}
</div>
);
}
9. Signing and Executing Transactions
Use useDAppKit and call signAndExecuteTransaction as an async function. Build the Transaction exactly as you would in any other context โ see sui-ts-sdk for PTB construction patterns:
import { useDAppKit, useCurrentClient, useCurrentAccount } from '@mysten/dapp-kit-react';
import { Transaction } from '@mysten/sui/transactions';
import { useState } from 'react';
function ActionButton() {
const dAppKit = useDAppKit();
const client = useCurrentClient();
const account = useCurrentAccount();
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleAction = async () => {
if (!account) return;
setIsPending(true);
setError(null);
try {
const tx = new Transaction();
const result = await dAppKit.signAndExecuteTransaction({ transaction: tx });
if (result.FailedTransaction) {
throw new Error(result.FailedTransaction.status.error?.message ?? 'Transaction failed');
}
const digest = result.Transaction.digest;
await client.waitForTransaction({ digest });
} catch (e) {
setError(e instanceof Error ? e.message : 'Unknown error');
} finally {
setIsPending(false);
}
};
return (
<>
<button onClick={handleAction} disabled={!account || isPending}>
{isPending ? 'Waiting for wallet...' : 'Submit'}
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</>
);
}
Key differences from the old API:
- No mutation hook โ
signAndExecuteTransaction is a plain async function on the useDAppKit() instance
- Result is a discriminated union: check
result.FailedTransaction for failure; success data is at result.Transaction.digest
- Manage your own
isPending / error state with useState
10. Signing Without Executing
When you need the user's signature but execution happens elsewhere (e.g., a sponsored flow where your backend attaches gas):
import { useDAppKit } from '@mysten/dapp-kit-react';
import { Transaction } from '@mysten/sui/transactions';
function SponsoredMint() {
const dAppKit = useDAppKit();
const handleSign = async () => {
const tx = new Transaction();
const { bytes, signature } = await dAppKit.signTransaction({ transaction: tx });
await fetch('/api/sponsor', {
method: 'POST',
body: JSON.stringify({ bytes, signature }),
});
};
return <button onClick={handleSign}>Mint (Gasless)</button>;
}
For the server-side sponsored execution flow (attaching the sponsor's gas, collecting both signatures, submitting), see sui-ts-sdk ยง15.
11. Personal Message Signing
import { useDAppKit, useCurrentAccount } from '@mysten/dapp-kit-react';
function AuthButton() {
const dAppKit = useDAppKit();
const account = useCurrentAccount();
const handleAuth = async () => {
if (!account) return;
const nonce = crypto.randomUUID();
const message = new TextEncoder().encode(`Sign in to MyApp: nonce=${nonce}`);
const { bytes, signature } = await dAppKit.signPersonalMessage({ message });
await verifyOnServer({ address: account.address, bytes, signature });
};
return (
<button onClick={handleAuth} disabled={!account}>
Sign In
</button>
);
}
The message must be a Uint8Array โ use TextEncoder to convert strings. Always display message content clearly to users before signing.
12. Network Switching
Read the active network with useCurrentNetwork; switch it via useDAppKit:
import { useCurrentNetwork, useDAppKit } from '@mysten/dapp-kit-react';
function NetworkSwitcher() {
const network = useCurrentNetwork();
const dAppKit = useDAppKit();
return (
<select value={network} onChange={(e) => dAppKit.switchNetwork(e.target.value)}>
<option value="mainnet">Mainnet</option>
<option value="testnet">Testnet</option>
</select>
);
}
Only networks in createDAppKit's networks array are valid targets. switchNetwork executes synchronously and does not notify the wallet โ it only affects the dApp's client.
13. Cache Invalidation After Transactions
After a successful transaction, invalidate React Query caches so the UI reflects updated state. Always wait for indexing first:
import { useQueryClient } from '@tanstack/react-query';
import { useDAppKit, useCurrentClient, useCurrentAccount } from '@mysten/dapp-kit-react';
function MintButton() {
const dAppKit = useDAppKit();
const client = useCurrentClient();
const account = useCurrentAccount();
const queryClient = useQueryClient();
const handleMint = async () => {
const tx = new Transaction();
const result = await dAppKit.signAndExecuteTransaction({ transaction: tx });
if (result.FailedTransaction) throw new Error('Mint failed');
await client.waitForTransaction({ digest: result.Transaction.digest });
await queryClient.invalidateQueries({ queryKey: ['getBalance', account?.address] });
await queryClient.invalidateQueries({ queryKey: ['getOwnedObjects', account?.address] });
};
return <button onClick={handleMint}>Mint NFT</button>;
}
const result = await dAppKit.signAndExecuteTransaction({ transaction: tx });
await queryClient.invalidateQueries(...);
await client.waitForTransaction({ digest: result.Transaction.digest });
14. Wallet-Gated UI
import { useCurrentAccount, ConnectButton } from '@mysten/dapp-kit-react';
function ProtectedPage() {
const account = useCurrentAccount();
if (!account) {
return (
<div>
<p>Connect your wallet to continue.</p>
<ConnectButton />
</div>
);
}
return <Dashboard address={account.address} />;
}
Reusable guard:
function WalletGuard({ children }: { children: React.ReactNode }) {
const account = useCurrentAccount();
if (!account) return <ConnectButton />;
return <>{children}</>;
}
15. What dApp Kit is NOT
| Mistake | Correct approach |
|---|
Using @mysten/dapp-kit in new projects | That package is deprecated; use @mysten/dapp-kit-react (React) or @mysten/dapp-kit-core (Vue, vanilla JS, other frameworks) |
Using SuiJsonRpcClient in createClient | The new dApp Kit uses SuiGrpcClient โ pass it to createDAppKit's createClient |
Three-provider setup (QueryClientProvider + SuiClientProvider + WalletProvider) | Use createDAppKit + DAppKitProvider โ the old provider pattern is gone |
Omitting the declare module augmentation | Without it, useDAppKit() and hooks lose TypeScript type inference |
useSignAndExecuteTransaction, useConnectWallet, useDisconnectWallet | These mutation hooks no longer exist; use useDAppKit() instance methods instead |
useSuiClient | Renamed to useCurrentClient |
useSuiClientContext | Replaced by useCurrentNetwork (read) + useDAppKit().switchNetwork() (write) |
useSuiClientQuery / useSuiClientInfiniteQuery | Removed; use useCurrentClient + useQuery/useInfiniteQuery from @tanstack/react-query |
Checking result.digest after signAndExecuteTransaction | Result is a discriminated union: use result.Transaction.digest (success) or result.FailedTransaction (failure) |
Reading account.address without null check | useCurrentAccount() returns null before connection; always guard |
enabled: !!account omitted from queries | Without it, the query fires with an undefined owner and errors immediately |
Invalidating queries before waitForTransaction | Indexer may not have processed the tx yet; always wait first |
ConnectButton in SSR without client-side guard | Wallet detection is browser-only; ensure client-side rendering for wallet components |