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:

  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, TON, Cosmos, Aleo, Stellar — 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

Package Mapping

Extension Notes

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. Install @tanstack/react-query alongside @dynamic-labs-sdk/react-hooks — it is a required peer dependency.
  3. Wrap the React tree in <QueryClientProvider client={queryClient}> (outer) and <DynamicProvider client={dynamicClient}> (inner) from @dynamic-labs-sdk/react-hooks.
  4. Read state with hooks from @dynamic-labs-sdk/react-hooks: useUser, useGetWalletAccounts, useGetAvailableWalletProvidersData, useInitStatus, useSessionExpiresAt, useGetUserSocialAccounts, useDynamicClient. They auto-subscribe and trigger re-renders.
  5. For one-off side-effect listeners (e.g. WaaS bootstrap on userChanged), use useOnEvent. 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:

useOnEvent({
  event: "userChanged",
  listener: async ({ user }) => {
    if (!user) return;
    const missingChains = getChainsMissingWaasWalletAccounts();
    if (missingChains.length > 0) {
      await createWaasWalletAccounts({ chains: missingChains });
    }
  },
});

Valid Combinations

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