Skip to main content

Documentation Index

Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt

Use this file to discover all available pages before exploring further.

Use the JavaScript SDK inside a React app. State is wired via the @dynamic-labs-sdk/react-hooks package.
Before you start: a Vite-based React app and a Dynamic environment ID from the Dynamic dashboard.
Add the Dynamic docs MCP to your AI editor first — your agent can then query the docs directly. Instructions are on the Welcome page.
Copy this prompt into your LLM (e.g. Claude, ChatGPT, Cursor) to implement Dynamic in your React app via the JS SDK.
# React + JavaScript SDK Quickstart

## 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

```
npm i @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @dynamic-labs-sdk/react-hooks
```

### Step 3 — Create the Dynamic client module

Create `src/dynamicClient.ts`. Importing this file at app root registers extensions before any component renders.

```typescript
import { createDynamicClient, initializeClient } from "@dynamic-labs-sdk/client";
import { addEvmExtension } from "@dynamic-labs-sdk/evm";

export const dynamicClient = createDynamicClient({
  autoInitialize: false,
  environmentId: "YOUR_ENVIRONMENT_ID",
  metadata: {
    name: "My App",
    // IMPORTANT: the property is `universalLink`, not `url`
    universalLink: window.location.origin,
  },
});

// Register extensions immediately, before initialization completes.
// Extension functions take NO arguments — do not pass the client instance.
addEvmExtension();

void initializeClient();
```

### Step 4 — Wrap the app in `DynamicProvider`

Update `src/main.tsx` to import the client module (registering extensions) and wrap the tree:

```tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { DynamicProvider } from "@dynamic-labs-sdk/react-hooks";

import { App } from "./App";
import { dynamicClient } from "./dynamicClient";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <DynamicProvider client={dynamicClient}>
      <App />
    </DynamicProvider>
  </StrictMode>
);
```

### Step 5 — Email OTP login component

Build a minimal login form. The JS SDK is headless — there is no built-in modal.

```tsx
import { useState } from "react";
import { sendEmailOTP, verifyOTP } from "@dynamic-labs-sdk/client";
import type { OtpVerification } from "@dynamic-labs-sdk/client";

export function Login() {
  const [email, setEmail] = useState("");
  const [code, setCode] = useState("");
  const [pending, setPending] = useState<OtpVerification | null>(null);

  const handleSendCode = async () => {
    const { otpVerification } = await sendEmailOTP({ email });
    setPending(otpVerification);
  };

  const handleVerify = async () => {
    if (!pending) return;
    // IMPORTANT: the parameter is `verificationToken`, not `otp`
    await verifyOTP({ otpVerification: pending, verificationToken: code });
    setPending(null);
  };

  if (!pending) {
    return (
      <>
        <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="email" />
        <button onClick={handleSendCode}>Send code</button>
      </>
    );
  }

  return (
    <>
      <input value={code} onChange={(e) => setCode(e.target.value)} placeholder="123456" />
      <button onClick={handleVerify}>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 `useEvent` 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.

```tsx
import { useEvent } 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() {
  useEvent({
    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`.**

```tsx
import { useUser, useWalletAccounts, useInitStatus } from "@dynamic-labs-sdk/react-hooks";

export function Dashboard() {
  const initStatus = useInitStatus();
  const user = useUser();
  const accounts = useWalletAccounts();

  if (initStatus !== "initialized") 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

```tsx
import { logout } from "@dynamic-labs-sdk/client";

export function LogoutButton() {
  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:**

1. Which package manager do you prefer? (npm, yarn, pnpm, bun)
2. Which chains do you want to support? (EVM, Solana, Sui, Aptos, Bitcoin, Tron, Starknet, Cosmos — one or more)
3. If EVM or Solana: do you need only embedded wallets (smaller bundle) or the full extension including external wallet discovery?
4. If EVM or Solana: do you need WalletConnect for cross-device connections (QR code / deep link)?
5. 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 i`
- `yarn`: `yarn add`
- `pnpm`: `pnpm add`
- `bun`: `bun add`

### Package Mapping
- Core (always required): `@dynamic-labs-sdk/client`
- React state hooks (always required for React apps): `@dynamic-labs-sdk/react-hooks`
- 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`
- Cosmos: `@dynamic-labs-sdk/cosmos`
- WalletConnect (EVM/Solana only, optional): use `addWalletConnectEvmExtension` from `@dynamic-labs-sdk/evm/wallet-connect`, `addWalletConnectSolanaExtension` from `@dynamic-labs-sdk/solana/wallet-connect`. See [WalletConnect Integration](/javascript/reference/wallets/walletconnect-integration).

### Extension Notes
- **Default extensions** (`addEvmExtension`, `addSolanaExtension`) bundle external wallet discovery + embedded (WaaS) support
- **Standalone embedded-only extensions** (`addWaasEvmExtension`, `addWaasSolanaExtension`) produce a smaller bundle — use when the user only needs embedded wallets
- Extension functions take **NO arguments** — do not pass the client instance (e.g. `addEvmExtension()` not `addEvmExtension(client)`)
- Register extensions immediately after `createDynamicClient()`, before initialization completes
- See [Adding EVM Extensions](/javascript/reference/evm/adding-evm-extensions) and [Adding Solana Extensions](/javascript/reference/solana/adding-solana-extensions) for the full list of standalone options

### React Wiring (apply to all custom setups)

Always do these in any React + JS SDK app:

1. 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.
2. Wrap the React tree in `<DynamicProvider client={dynamicClient}>` from `@dynamic-labs-sdk/react-hooks`.
3. Read state with hooks from `@dynamic-labs-sdk/react-hooks`: `useUser`, `useWalletAccounts`, `useWalletProviders`, `useInitStatus`, `useSessionExpiresAt`, `useSocialAccounts`, `useDynamicClient`. They auto-subscribe and trigger re-renders.
4. For one-off side-effect listeners (e.g. WaaS bootstrap on `userChanged`), use `useEvent`. Do not call `onEvent` directly 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 `useEvent` on `userChanged`:
  ```tsx
  useEvent({
    event: "userChanged",
    listener: async (user) => {
      if (!user) return;
      const missingChains = getChainsMissingWaasWalletAccounts();
      if (missingChains.length > 0) {
        await createWaasWalletAccounts({ chains: missingChains });
      }
    },
  });
  ```
- **Wallet address** — read via `useWalletAccounts()[0]?.address` (not `accountAddress`)
- **OTP verification** — parameter is `verificationToken`, not `otp`
- **Client metadata** — property is `universalLink`, not `url`

### Valid Combinations
- Any single chain, or any combination of two or more chains
- At least one chain must be selected
- `@dynamic-labs-sdk/react-hooks` is included in every React combination

### Documentation
All docs for this SDK: https://docs.dynamic.xyz (paths starting with `/javascript/`)
Environment ID: https://app.dynamic.xyz/dashboard/developer/api

---

## Critical API Reference (apply to both paths)

| Correct | Incorrect | Notes |
|---|---|---|
| `metadata: { universalLink: window.location.origin }` | `metadata: { url: window.location.origin }` | Renamed in v0.24+ |
| `<DynamicProvider client={dynamicClient}>…</DynamicProvider>` | Importing hooks without a provider | `useUser`, `useWalletAccounts`, etc. require `DynamicProvider` in the tree |
| `useEvent({ event, listener })` inside a component | `onEvent({ event, listener }, client)` inside a component | `useEvent` cleans up on unmount; `onEvent` will leak |
| `addEvmExtension()` | `addEvmExtension(client)` | Extension functions take no arguments |
| `verifyOTP({ otpVerification, verificationToken: '123456' })` | `verifyOTP({ otpVerification, otp: '123456' })` | Parameter is `verificationToken` not `otp` |
| `wallet.address` | `wallet.accountAddress` | Use `address` on objects from `useWalletAccounts()` |
| 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.

| Hook | Returns | Re-renders on |
|---|---|---|
| `useInitStatus()` | `'initializing' \| 'initialized' \| 'error'` | `initStatusChanged` |
| `useUser()` | The current `User` object or `null` | `userChanged` |
| `useWalletAccounts()` | `WalletAccount[]` | `walletAccountsChanged` |
| `useWalletProviders()` | Available wallet providers list | `walletProviderChanged` |
| `useSessionExpiresAt()` | Session expiry timestamp | `userChanged` |
| `useSocialAccounts()` | Linked social accounts | `userChanged` |
| `useDynamicClient()` | The `dynamicClient` from context | Never (stable reference) |
| `useEvent({ event, listener })` | `void` | Fires `listener` on each event; cleans up on unmount |

---

## Building a headless integration?

The JavaScript SDK is always headless. See the [Authentication screens](/javascript/authentication-methods/headless-authentication) 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](https://app.dynamic.xyz/dashboard/developer/api).

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](/javascript/authentication-methods/step-up-auth/overview) for the full implementation guide.

---

## Device Registration

**Required before accepting the `2026_04_01` API version** — verify your
minimum API version in [Dashboard > Developers > API & SDK Keys](https://app.dynamic.xyz/dashboard/developer/api).

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 on `useInitStatus() === 'initialized'`.
- **`DynamicProvider` must 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.
- **Do not call `onEvent` inside component bodies.** Use `useEvent` from `@dynamic-labs-sdk/react-hooks` — it deduplicates subscriptions and cleans up on unmount.
- **Strict Mode double-invokes effects in development.** `useEvent` handles this correctly; raw `useEffect(() => onEvent(...), [])` will register two listeners under Strict Mode.
- **Don't recreate `dynamicClient` per render.** It must live in a module-level singleton (e.g. `src/dynamicClient.ts`) — never in component state.