@dynamic-labs-sdk/react-hooks package — the same headless client and hooks as on web, plus native polyfills and deep links.
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.
Before you start: a React Native app (Expo development build or bare) and a Dynamic environment ID from the Dynamic dashboard. Expo Go is not supported.
- Agent-friendly
- Manual
Add the Dynamic docs MCP to your AI editor first — your agent can then query the docs directly.
- Cursor
- Claude Code
- Codex
- Other
claude mcp add --transport http dynamic https://www.dynamic.xyz/docs/mcp
codex mcp add dynamic --url https://www.dynamic.xyz/docs/mcp
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.# React Native Quickstart (JS SDK)
## Default for React Native apps
**This is the recommended quickstart for React Native.** Use this document—not the Legacy React Native SDK quickstart (`/react-native/reference/quickstart`, agent prompt id: `react-native`)—when integrating Dynamic into a new or existing React Native app unless the user already uses `@dynamic-labs/client` + `ReactNativeExtension` or explicitly requests the legacy SDK.
## Purpose and Usage
This document provides structured quickstart information for using the Dynamic JavaScript SDK inside a React Native app (Expo or bare). It is designed to be consumed by LLMs to generate accurate installation commands and code examples based on user requirements.
Key facts:
- Same headless `@dynamic-labs-sdk/client` as web
- State flows through `@dynamic-labs-sdk/react-hooks` (same as React web)
- React Native adds polyfills, native modules, and deep-link metadata only
- Do **not** install `@dynamic-labs/client` or `@dynamic-labs/react-native-extension` (legacy)
**How to use this document:**
Start by asking the user which path they want:
> "Would you like a **Quick start** (Expo + email login + EVM + embedded wallet, zero configuration — recommended if you're just getting started) or a **Custom setup** (choose Expo vs bare, 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:** Expo (development build) + npm + EVM only + email OTP + embedded WaaS wallet + `@dynamic-labs-sdk/react-hooks`
### Step 1 — Prerequisites
Tell the user:
- Node.js 18+ is required
- An Expo app is required (e.g. `npx create-expo-app@latest`)
- **Expo Go is not supported** — the SDK needs native modules linked via a development build or EAS Build
- 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
```
npx expo install @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @dynamic-labs-sdk/react-hooks @tanstack/react-query @react-native-async-storage/async-storage react-native-keychain expo-linking expo-crypto buffer
```
`@tanstack/react-query` is a required peer dependency of `@dynamic-labs-sdk/react-hooks`.
### Step 3 — Create polyfills
Create `polyfills.ts` at the project root:
```typescript
import { Buffer } from 'buffer';
import { getRandomValues, randomUUID } from 'expo-crypto';
if (!global.crypto) {
// @ts-expect-error — partial Crypto implementation
global.crypto = {};
}
if (!global.crypto.getRandomValues) {
// @ts-expect-error — partial Crypto implementation
global.crypto.getRandomValues = getRandomValues;
}
if (!global.crypto.randomUUID) {
// @ts-expect-error — partial Crypto implementation
global.crypto.randomUUID = randomUUID;
}
global.Buffer = Buffer;
```
### Step 4 — Load polyfills first
Create `index.ts` at the project root and point `package.json` `"main"` at it:
```typescript
// Polyfills must run before any other import
import './polyfills';
import 'expo-router/entry';
```
```json
{
"main": "./index.ts"
}
```
If the app does not use Expo Router, import `./polyfills` as the first line of the existing entry file instead.
### Step 5 — Create the Dynamic client module
Create `src/dynamicClient.ts` (or project-root `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",
// Deep link scheme registered in the native app (Info.plist / AndroidManifest / app.json)
nativeLink: "myapp://",
// App origin — required for passkeys; use your real HTTPS origin in production
universalLink: "https://example.com",
},
});
// Register extensions immediately, before initialization completes.
// Extension functions take NO arguments — do not pass the client instance.
addEvmExtension();
void initializeClient();
```
### Step 6 — Wrap the app in `QueryClientProvider` and `DynamicProvider`
At the root layout (e.g. `app/_layout.tsx` for Expo Router), import the client module and wrap the tree. Mount `QueryClientProvider` outside `DynamicProvider`:
```tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { DynamicProvider } from "@dynamic-labs-sdk/react-hooks";
import { dynamicClient } from "./dynamicClient";
const queryClient = new QueryClient();
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<DynamicProvider client={dynamicClient}>
{children}
</DynamicProvider>
</QueryClientProvider>
);
}
```
### Step 7 — 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 { View, TextInput, Button } from "react-native";
import { useSendEmailOTP, useVerifyOTP } from "@dynamic-labs-sdk/react-hooks";
export function Login() {
const [email, setEmail] = useState("");
const [code, setCode] = useState("");
const { mutate: sendEmailOTP, data: sendResult } = useSendEmailOTP();
const { mutate: verifyOTP } = useVerifyOTP();
const otpVerification = sendResult?.otpVerification;
if (!otpVerification) {
return (
<View>
<TextInput value={email} onChangeText={setEmail} placeholder="email" autoCapitalize="none" />
<Button title="Send code" onPress={() => sendEmailOTP({ email })} />
</View>
);
}
return (
<View>
<TextInput value={code} onChangeText={setCode} placeholder="123456" keyboardType="number-pad" />
<Button
title="Verify"
onPress={() => verifyOTP({ otpVerification, verificationToken: code })}
/>
</View>
);
}
```
### Step 8 — 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()` 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 { useOnEvent } from "@dynamic-labs-sdk/react-hooks";
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 9 — 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 { Text, View } from "react-native";
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 <Text>Loading…</Text>;
if (!user) return <Text>Not signed in</Text>;
const address = accounts[0]?.address;
return (
<View>
<Text>Signed in as {user.email}</Text>
<Text>Wallet: {address}</Text>
</View>
);
}
```
### Step 10 — Rebuild the native app
After installing native modules, rebuild so they are linked:
```
npx expo prebuild --clean
npx expo run:ios
# or: npx expo run:android
```
---
## 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. Are you using **Expo** or **bare React Native** (`@react-native-community/cli`)?
2. Which package manager do you prefer? (npm, yarn, pnpm, bun — use `npx expo install` when on Expo)
3. Which chains do you want to support? (EVM, Solana, Sui, Aptos, Bitcoin, Tron, Starknet, TON, Cosmos — one or more)
4. If EVM or Solana: do you need only embedded wallets (smaller bundle) or the full extension including external wallet discovery?
5. If EVM or Solana: do you need WalletConnect for external wallet connections?
6. 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
- Expo: `npx expo install`
- `npm`: `npm i` / `npm install`
- `yarn`: `yarn add`
- `pnpm`: `pnpm add`
- `bun`: `bun add`
### Package Mapping
- Core (always required): `@dynamic-labs-sdk/client`
- React state hooks (always required): `@dynamic-labs-sdk/react-hooks` and `@tanstack/react-query`
- Storage / keychain (always required on RN): `@react-native-async-storage/async-storage`, `react-native-keychain`
- 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`
- Social / OAuth (optional): `react-native-inappbrowser-reborn`
- Passkeys (optional): `react-native-passkey` — see https://docs.dynamic.xyz/javascript/react-native/passkeys
- WalletConnect (optional): `@walletconnect/react-native-compat` (+ `@react-native-community/netinfo` on bare)
- Export WaaS private key (optional): `@dynamic-labs-sdk/react-native-embedded-wallet-export`, `react-native-webview`
### Expo-specific setup
- Polyfills: `expo-crypto`, `buffer` — create `polyfills.ts` with crypto + Buffer shims (see Quick Start Path)
- Entry: custom `index.ts` that imports `./polyfills` first, then `expo-router/entry` (or your entry); set `"main": "./index.ts"`
- Add `expo-linking` when you need deep links / social return
- Rebuild: `npx expo prebuild --clean` then `npx expo run:ios` / `npx expo run:android`
- Full guide: https://docs.dynamic.xyz/javascript/react-native/expo
### Bare React Native–specific setup
- Polyfills: `react-native-get-random-values`, `buffer`
- Babel: add `@babel/plugin-transform-export-namespace-from` and `@babel/plugin-transform-class-static-block`, then restart Metro with `--reset-cache`
- Create `polyfills.ts` with random-values import first, Buffer, `crypto.randomUUID` shim, and a `window.location` / `globalThis.location` shim whose origin equals `metadata.universalLink`
- Import `./polyfills` as the **first** line of `index.js`
- Register deep link scheme in `Info.plist` and `AndroidManifest.xml`; add Android `<queries>` for `https`/`wc` when using WalletConnect; add JitPack for WalletConnect
- Rebuild: `cd ios && bundle exec pod install && cd ..` then `npx react-native run-ios` / `run-android`
- Full guide: https://docs.dynamic.xyz/javascript/react-native/bare-react-native
### Extension Notes
- **Default extensions** (`addEvmExtension`, `addSolanaExtension`, `addTonExtension`) bundle external wallet discovery + embedded (WaaS) support
- **Standalone embedded-only extensions** (`addWaasEvmExtension`, `addWaasSolanaExtension`, `addWaasTonExtension`) produce a smaller bundle — use when the user only needs embedded wallets
- Extension functions take **NO arguments** — do not pass the client instance
- Register extensions immediately after `createDynamicClient()`, before initialization completes
### React Native wiring (apply to all custom setups)
1. Create polyfills and load them before any other app code.
2. Create the client and register extensions in a single module (`dynamicClient.ts`). Set `metadata.nativeLink` (deep link scheme) and `metadata.universalLink` (HTTPS origin). Import this module at app root.
3. Install `@tanstack/react-query` alongside `@dynamic-labs-sdk/react-hooks`.
4. Wrap the tree in `<QueryClientProvider>` (outer) and `<DynamicProvider client={dynamicClient}>` (inner).
5. Read state with hooks from `@dynamic-labs-sdk/react-hooks`. For one-off listeners, use `useOnEvent` — do not call `onEvent` inside component bodies.
6. Rebuild the native app after adding native modules.
### WalletConnect on React Native
Same extensions as web, but open wallets with `Linking.openURL` (from `react-native`, or `expo-linking`) instead of `window.open`, and skip the QR code — mobile deep links straight to the wallet app. Import `@walletconnect/react-native-compat` as the **first** line of `polyfills.ts`. See https://docs.dynamic.xyz/javascript/reference/wallets/walletconnect-integration.
### Post-Auth Patterns (embedded wallets)
- **WaaS wallet creation** — not automatic. Trigger from `useOnEvent` on `userChanged` with `getChainsMissingWaasWalletAccounts()` / `createWaasWalletAccounts()`.
- **State hooks return the react-query result** — read the value off `data`, e.g. `const { data: accounts = [] } = useGetWalletAccounts()` then `accounts[0]?.address`
- **OTP verification** — parameter is `verificationToken`, not `otp`
- **Client metadata** — use `universalLink` and `nativeLink`, not `url`
### Documentation
All docs for this SDK: https://docs.dynamic.xyz (paths starting with `/javascript/`)
Environment ID: https://app.dynamic.xyz/dashboard/developer/api
Expo setup: https://docs.dynamic.xyz/javascript/react-native/expo
Bare setup: https://docs.dynamic.xyz/javascript/react-native/bare-react-native
---
## Critical API Reference (apply to both paths)
| Correct | Incorrect | Notes |
|---|---|---|
| `@dynamic-labs-sdk/client` | `@dynamic-labs/client` | Legacy RN SDK — different API |
| `metadata: { nativeLink, universalLink }` | `metadata: { url }` | RN needs both links |
| `<QueryClientProvider>` + `<DynamicProvider>` | Hooks without providers | Required for all hooks |
| `useOnEvent({ event, listener })` | `onEvent` inside components | Prevents subscription leaks |
| `addEvmExtension()` | `addEvmExtension(client)` | Extension functions take no arguments |
| `verifyOTP({ otpVerification, verificationToken })` | `verificationToken` misspelled as `otp` | Parameter is `verificationToken` |
| `wallet.address` | `wallet.accountAddress` | Use `address` from `useGetWalletAccounts()` |
| Development build / `expo prebuild` | Expo Go | Native modules are not linked in Expo Go |
---
## 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.**
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.**
The JavaScript SDK is always headless — handle device registration manually.
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, 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**.
### 2 — Login method not enabled
Email OTP (or whichever login method the app uses) must be toggled on under **Sign-in Methods**.
### 3 — Embedded wallets not enabled
If the app uses WaaS embedded wallets, enable **Embedded Wallets** under **Wallets**.
### 4 — Origins / deep links
Add your app's HTTPS origin (`metadata.universalLink`) to **Allowed Origins** under **Security**. Ensure `metadata.nativeLink` matches the scheme registered in the native project.
If the app is still not working, check Metro / device logs and refer to https://docs.dynamic.xyz/overview/troubleshooting/general. Platform setup details: https://docs.dynamic.xyz/javascript/react-native/setup
---
## React Native–Specific Pitfalls
- **Expo Go will not work.** Use a development build or bare workflow.
- **Polyfills must load first.** Importing them after other modules can break crypto and embedded wallets.
- **Hooks return `null`/empty before init completes.** Gate UI on `useInitStatus().data === 'finished'`.
- **`DynamicProvider` must wrap the entire tree that uses hooks.**
- **`DynamicProvider` does not include `QueryClientProvider`.** Mount both; put `QueryClientProvider` outside `DynamicProvider`.
- **Do not call `onEvent` inside component bodies.** Use `useOnEvent`.
- **Don't recreate `dynamicClient` per render.** Module-level singleton only.
- **Passkeys need extra native setup** beyond installing the package — see https://docs.dynamic.xyz/javascript/react-native/passkeys
After the embed
- Platform setup: React Native Setup, Expo, Bare React Native.
- Full client setup: Creating a Dynamic Client, Initializing the Dynamic Client.
- Add chains with EVM, Solana, or all extension types.
- Reactive patterns: React Hooks.
- Something wrong? See Troubleshooting.
Key patterns
After polyfills and the client are in place, React Native uses the same hooks as React on web:import { useGetWalletAccounts, useUser, useInitStatus } from '@dynamic-labs-sdk/react-hooks';
function Dashboard() {
const { data: walletAccounts = [] } = useGetWalletAccounts();
const { data: user } = useUser();
const { data: initStatus } = useInitStatus();
// re-renders automatically when wallets, user, or init status change
}
Import your
dynamicClient.ts module at the root of your app so extensions are registered before any component renders. Set metadata.nativeLink and metadata.universalLink when creating the client.