react quickstart.md
React Quickstart (JS SDK)
Use the JavaScript SDK inside a React app. State is wired via the @dynamic-labs-sdk/react-hooks package.
Warning
The JavaScript SDK is headless — there is no built-in modal, wallet picker, or
login form. You build all UI yourself. See the JavaScript SDK Overview for details.
Note
Before you start: a Vite-based React app and a Dynamic environment ID from the Dynamic dashboard.
Agent-friendly
Tip
Add the Dynamic docs MCP to your AI editor first — your agent can then query the docs directly.
Cursor
Click here to add the MCP to Cursor.
Claude Code
theme={"system"}
claude mcp add --transport http dynamic https://www.dynamic.xyz/docs/mcp
Codex
theme={"system"}
codex mcp add dynamic --url https://www.dynamic.xyz/docs/mcp
Other
If your editor or agent supports remote HTTP MCP, add a server that points at https://www.dynamic.xyz/docs/mcp. The exact steps depend on the product; use its MCP documentation for URL-based or HTTP transport setup.
Copy this prompt into your LLM (e.g. Claude, ChatGPT, Cursor) to implement Dynamic in your React app via the JS SDK.
theme={"system"}
# React SDK Quickstart
## Default for React apps
**This is the recommended quickstart for React.** Use this document—not the React SDK quickstart (`/react/reference/quickstart`, agent prompt id: `react`)—when integrating Dynamic into a new or existing React app unless the user already uses `@dynamic-labs/sdk-react-core` or explicitly requests the old React SDK.
## Purpose and Usage
This document provides structured quickstart information for using the Dynamic JavaScript SDK inside a React app. It is designed to be consumed by LLMs to generate accurate installation commands and code examples based on user requirements.
Key fact: state flows through `@dynamic-labs-sdk/react-hooks`.
**How to use this document:**
Start by asking the user which path they want:
> "Would you like a **Quick start** (email login + EVM + embedded wallet, zero configuration — recommended if you're just getting started) or a **Custom setup** (choose your chains, auth method, and wallet type)?"
- If the user chooses **Quick start**: follow the Quick Start Path section exactly. Do not ask any configuration questions — execute the happy path as written.
- If the user chooses **Custom setup**: follow the Custom Setup Path section and ask all configuration questions before proceeding.
## Quick Start Path
> Use this path when the user chooses Quick start. Execute every step in order. Do not skip steps. Do not ask configuration questions.
**Stack:** Vite + React 18 + npm + EVM only + email OTP + embedded WaaS wallet + `@dynamic-labs-sdk/react-hooks`
### Step 1 — Prerequisites
Tell the user:
- Node.js 18+ is required
- A Vite + React project is required (e.g. `npm create vite@latest my-app -- --template react-ts`)
- A Dynamic environment ID is required — get one at https://app.dynamic.xyz/dashboard/developer/api
Ask the user for their environment ID before proceeding.
### Step 2 — Install
```bash
npm i @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @dynamic-labs-sdk/react-hooks @tanstack/react-query
@tanstack/react-query is a required peer dependency of @dynamic-labs-sdk/react-hooks. Every state, query, and mutation hook is built on TanStack Query.
Step 3 — Create the Dynamic client module
Create src/dynamicClient.ts. Importing this file at app root registers extensions before any component renders.
import { createDynamicClient } from "@dynamic-labs-sdk/client";
import { addEvmExtension } from "@dynamic-labs-sdk/evm";
export const dynamicClient = createDynamicClient({
environmentId: "YOUR_ENVIRONMENT_ID",
metadata: {
name: "My App",
// IMPORTANT: the property is `universalLink`, not `url`
universalLink: window.location.origin,
},
});
// Register extensions immediately after createDynamicClient().
// The client argument is optional: omit it unless you run multiple clients.
addEvmExtension();
Step 4 — Wrap the app in QueryClientProvider and DynamicProvider
Update src/main.tsx to import the client module (registering extensions) and wrap the tree. DynamicProvider supplies the Dynamic client context; QueryClientProvider is required because every hook in @dynamic-labs-sdk/react-hooks uses TanStack Query internally. Mount QueryClientProvider outside DynamicProvider:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { DynamicProvider } from "@dynamic-labs-sdk/react-hooks";
import { App } from "./App";
import { dynamicClient } from "./dynamicClient";
const queryClient = new QueryClient();
createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<DynamicProvider client={dynamicClient}>
<App />
</DynamicProvider>
</QueryClientProvider>
</StrictMode>
);
Step 5 — Email OTP login component
Build a minimal login form. The JS SDK is headless — there is no built-in modal.
import { useState } from "react";
import { useSendEmailOTP, useVerifyOTP } from "@dynamic-labs-sdk/react-hooks";
export function Login() {
const [email, setEmail] = useState("");
const [code, setCode] = useState("");
const { mutate: sendEmailOTP, data: otpVerification } = useSendEmailOTP();
const { mutate: verifyOTP } = useVerifyOTP();
if (!otpVerification) {
return (
<>
<input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="email" />
<button onClick={() => sendEmailOTP({ email })}>Send code</button>
</>
);
}
return (
<>
<input value={code} onChange={(e) => setCode(e.target.value)} placeholder="123456" />
<button onClick={() => verifyOTP({ otpVerification, verificationToken: code })}>Verify</button>
</>
);
}
Step 6 — Create the WaaS wallet on auth success (required — not automatic)
After verifyOTP succeeds, the wallet does not exist yet. Subscribe once at app mount with useOnEvent and call createWaasWalletAccounts() unconditionally on userChanged. Do not guard this with an accounts.length === 0 check — the SDK may return a stale non-zero list immediately after auth, causing the creation step to be silently skipped.
import { useOnEvent } from "@dynamic-labs-sdk/react-hooks";
// WaaS functions are exported from the /waas subpath
import { createWaasWalletAccounts, getChainsMissingWaasWalletAccounts } from "@dynamic-labs-sdk/client/waas";
export function WaasBootstrap() {
useOnEvent({
event: "userChanged",
listener: async ({ user }) => {
if (!user) return;
const missingChains = getChainsMissingWaasWalletAccounts();
if (missingChains.length === 0) return;
await createWaasWalletAccounts({ chains: missingChains });
},
});
return null;
}
Mount <WaasBootstrap /> once inside DynamicProvider.
Step 7 — Display wallet state with hooks
Hooks from @dynamic-labs-sdk/react-hooks subscribe to client events and re-render automatically. Use wallet.address, not wallet.accountAddress.
import { useUser, useGetWalletAccounts, useInitStatus } from "@dynamic-labs-sdk/react-hooks";
export function Dashboard() {
const { data: initStatus } = useInitStatus();
const { data: user } = useUser();
const { data: accounts = [] } = useGetWalletAccounts();
if (initStatus !== "finished") return <p>Loading…</p>;
if (!user) return <p>Not signed in</p>;
const address = accounts[0]?.address;
return (
<>
<p>Signed in as {user.email}</p>
<p>Wallet: {address}</p>
</>
);
}
}
Step 8 — Logout
import { useLogout } from "@dynamic-labs-sdk/react-hooks";
export function LogoutButton() {
const { mutate: logout } = useLogout();
return <button onClick={() => logout()}>Log out</button>;
}
Custom Setup Path
Use this path when the user chooses Custom setup. Ask ALL questions below before generating any code.
Questions to ask the user:
- Which package manager do you prefer? (npm, yarn, pnpm, bun)
- Which chains do you want to support? (EVM, Solana, Sui, Aptos, Bitcoin, Tron, Starknet, TON, Cosmos, Aleo, Stellar — one or more)
- If EVM or Solana: do you need only embedded wallets (smaller bundle) or the full extension including external wallet discovery?
- If EVM or Solana: do you need WalletConnect for cross-device connections (QR code / deep link)?
- Which auth method? (email OTP, SMS OTP, social, external wallet, or a combination)
Only after receiving answers, use the sections below to generate the correct setup. Always include @dynamic-labs-sdk/react-hooks regardless of chain selection.
Package Manager Commands
npm:npm iyarn:yarn addpnpm:pnpm addbun:bun add
Package Mapping
- Core (always required):
@dynamic-labs-sdk/client - React state hooks (always required for React apps):
@dynamic-labs-sdk/react-hooksand@tanstack/react-query(peer dependency) - EVM:
@dynamic-labs-sdk/evm - Solana:
@dynamic-labs-sdk/solana - Sui:
@dynamic-labs-sdk/sui - Aptos:
@dynamic-labs-sdk/aptos - Bitcoin:
@dynamic-labs-sdk/bitcoin - Tron:
@dynamic-labs-sdk/tron - Starknet:
@dynamic-labs-sdk/starknet - TON:
@dynamic-labs-sdk/ton - Cosmos:
@dynamic-labs-sdk/cosmos - Aleo:
@dynamic-labs-sdk/aleo(no default extension: useaddWaasAleoExtensionfrom@dynamic-labs-sdk/aleo/waasfor embedded wallets, oraddAleoWalletStandardExtensionfrom@dynamic-labs-sdk/aleo/walletStandardfor external wallets) - Stellar:
@dynamic-labs-sdk/stellar - WalletConnect (EVM/Solana only, optional): use
addWalletConnectEvmExtensionfrom@dynamic-labs-sdk/evm/wallet-connect,addWalletConnectSolanaExtensionfrom@dynamic-labs-sdk/solana/wallet-connect. See WalletConnect Integration.
Extension Notes
- Default extensions (
addEvmExtension,addSolanaExtension,addTonExtension,addSuiExtension,addTronExtension,addBitcoinExtension) bundle external wallet discovery + embedded (WaaS) support - Standalone embedded-only extensions (
addWaasEvmExtension,addWaasSolanaExtension,addWaasTonExtension,addWaasSuiExtension,addWaasTronExtension,addWaasBitcoinExtension,addWaasAleoExtension) produce a smaller bundle. Use them when the user only needs embedded wallets. They live on the chain package's/waassubpath, for exampleimport { addWaasEvmExtension } from '@dynamic-labs-sdk/evm/waas'; - Every extension and SDK function takes an optional trailing client argument that defaults to the client created by
createDynamicClient(). Omit it unless the app runs multiple clients - Register extensions immediately after
createDynamicClient(), before initialization completes - See Adding EVM Extensions, Adding Solana Extensions, and Adding TON Extensions for the full list of standalone options
React Wiring (apply to all custom setups)
Always do these in any React + JS SDK app:
- Create the client and register extensions in a single module (
dynamicClient.ts). Import this module at app root so extensions are registered before any component renders. - Install
@tanstack/react-queryalongside@dynamic-labs-sdk/react-hooks— it is a required peer dependency. - Wrap the React tree in
<QueryClientProvider client={queryClient}>(outer) and<DynamicProvider client={dynamicClient}>(inner) from@dynamic-labs-sdk/react-hooks. - Read state with hooks from
@dynamic-labs-sdk/react-hooks:useUser,useGetWalletAccounts,useGetAvailableWalletProvidersData,useInitStatus,useSessionExpiresAt,useGetUserSocialAccounts,useDynamicClient. They auto-subscribe and trigger re-renders. - For one-off side-effect listeners (e.g. WaaS bootstrap on
userChanged), useuseOnEvent. Do not callonEventdirectly inside components — it will leak subscriptions across re-renders.
Post-Auth Patterns (apply to all custom setups that use embedded wallets)
These steps are required and are not in the client init — they must be added to your post-auth flow:
- WaaS wallet creation — not automatic. Trigger from a
useOnEventonuserChanged:
useOnEvent({
event: "userChanged",
listener: async ({ user }) => {
if (!user) return;
const missingChains = getChainsMissingWaasWalletAccounts();
if (missingChains.length > 0) {
await createWaasWalletAccounts({ chains: missingChains });
}
},
});
- State hooks return the react-query result — read the value off
data, e.g.const { data: accounts = [] } = useGetWalletAccounts()thenaccounts[0]?.address(notuseGetWalletAccounts()[0]and notaccountAddress) - OTP verification — parameter is
verificationToken, nototp - Client metadata — property is
universalLink, noturl
Valid Combinations
- Any single chain, or any combination of two or more chains
- At least one chain must be selected
@dynamic-labs-sdk/react-hooksis included in every React combination
Documentation
All docs for this SDK: https://docs.dynamic.xyz (paths starting with /javascript/)
Critical API Reference (apply to both paths)
| Correct | Incorrect | Notes |
|---|---|---|
metadata: { universalLink: window.location.origin } |
metadata: { url: window.location.origin } |
Renamed in v0.24+ |
<QueryClientProvider client={queryClient}>…</QueryClientProvider> |
Using hooks without TanStack Query | Throws No QueryClient set, use QueryClientProvider to set one on the first hook call |
<DynamicProvider client={dynamicClient}>…</DynamicProvider> |
Importing hooks without a provider | useUser, useGetWalletAccounts, etc. require DynamicProvider in the tree |
useOnEvent({ event, listener }) inside a component |
onEvent({ event, listener }, client) inside a component |
useOnEvent cleans up on unmount; onEvent will leak |
addEvmExtension() |
addEvmExtension(myClient) when there is one client |
The client argument is optional and defaults to the client you created |
verifyOTP({ otpVerification, verificationToken: '123456' }) |
verifyOTP({ otpVerification, otp: '123456' }) |
Parameter is verificationToken not otp |
wallet.address |
wallet.accountAddress |
Use address on objects from useGetWalletAccounts() |
Call createWaasWalletAccounts() unconditionally on userChanged |
Guard with accounts.length === 0 |
Stale list may cause silent skip |
Import dynamicClient.ts at app root (e.g. main.tsx) |
Import only inside components | Extensions must register before any component renders |
React Hooks Cheat Sheet
All from @dynamic-labs-sdk/react-hooks. Each hook subscribes internally and triggers a re-render on change — no manual event wiring needed. State hooks return the full react-query result ({ data, isLoading, error, refetch, ... }); the data column below is the type of data, which you destructure with an alias, e.g. const { data: user } = useUser().
| Hook | data |
Re-renders on |
|---|---|---|
useInitStatus() |
'uninitialized' | 'in-progress' | 'finished' | 'failed' |
initStatusChanged |
useUser() |
The current User object or null |
userChanged |
useGetWalletAccounts() |
WalletAccount[] |
walletAccountsChanged |
useGetAvailableWalletProvidersData() |
Available wallet providers list | walletProviderChanged |
useSessionExpiresAt() |
Session expiry timestamp | userChanged |
useGetUserSocialAccounts() |
Linked social accounts | userChanged |
useDynamicClient() |
Returns the client directly (const client = useDynamicClient()) |
Never (stable reference) |
useOnEvent({ event, listener }) |
No return value — subscribes a listener | Fires listener on each event; cleans up on unmount |
Building a headless integration?
The JavaScript SDK is always headless. See the Authentication screens for a full list of screens your app needs to handle.
Step-Up Authentication
Required before accepting the 2026_04_01 API version — verify your minimum API version in Dashboard > Developers > API & SDK Keys.
The JavaScript SDK is always headless — there is no built-in step-up UI. You must always handle step-up authentication manually: check requirements, call the appropriate verification method, and wait for the elevated token before proceeding with the sensitive operation.
See Step-up authentication for the full implementation guide.
Device Registration
The JavaScript SDK is always headless — there is no built-in device
registration UI. You must always handle device registration manually:
check whether the current device needs registration after auth, detect and
process the email verification redirect, and listen for completion events.
Full guide: https://docs.dynamic.xyz/javascript/authentication-methods/device-registration
Troubleshooting — Dashboard Configuration
If the app builds successfully but login fails, wallets don't appear, or you see network/auth errors, the most common causes are Dynamic dashboard settings that haven't been configured. Ask the user to verify each of the following in their Dynamic dashboard at https://app.dynamic.xyz:
1 — Chains not enabled
The EVM chain (or any other chain used in the quickstart) must be enabled under Chains & Networks in the dashboard. If the chain isn't toggled on, wallet creation and signing will silently fail or return empty results.
2 — Login method not enabled
Email OTP (or whichever login method the app uses) must be toggled on under Sign-in Methods. If it isn't enabled, the auth flow will fail at the point of sending the OTP.
3 — Embedded wallets not enabled
If the app uses WaaS embedded wallets, the Embedded Wallets feature must be enabled under Wallets in the dashboard. Without it, createWaasWalletAccounts() will return an error or produce no wallet.
4 — CORS origin not allowlisted
The URL the app is running on (e.g. http://localhost:5173) must be added to the Allowed Origins list in the dashboard under Security. Without it, all SDK requests will be blocked by CORS. Add the exact origin including port.
If all four are configured and the app is still not working, check the browser console for error codes and refer to https://docs.dynamic.xyz/overview/troubleshooting/general.
React-Specific Pitfalls
- Hooks return
null/empty before init completes. Gate UI onuseInitStatus().data === 'finished'. DynamicProvidermust wrap the entire tree that uses hooks. Mounting it inside a route or conditional will throw "useDynamicClient must be used within a DynamicProvider" inside any consumer outside that subtree.DynamicProviderdoes not includeQueryClientProvider. Mount both; putQueryClientProvideroutsideDynamicProvider.- Do not call
onEventinside component bodies. UseuseOnEventfrom@dynamic-labs-sdk/react-hooks— it deduplicates subscriptions and cleans up on unmount. - Strict Mode double-invokes effects in development.
useOnEventhandles this correctly; rawuseEffect(() => onEvent(...), [])will register two listeners under Strict Mode. - Don't recreate
dynamicClientper render. It must live in a module-level singleton (e.g.src/dynamicClient.ts) — never in component state.