For AI agents: a documentation index is available at /llms.txt. A markdown version of this page is available at the same URL with .md appended (or via Accept: text/markdown).
Skip to main content

Connect to EVM - React Native quickstart

Get started with MetaMask Connect EVM in your React Native or Expo dapp.

Steps

1. Create a new project

Create a new React Native or Expo project:

npx react-native@latest init MyProject

2. Install dependencies

Install MetaMask Connect EVM and its required polyfill packages:

npm install @metamask/connect-evm react-native-get-random-values buffer @react-native-async-storage/async-storage readable-stream

3. Create polyfills

Create polyfills.ts (at the project root or in src/) with all required global shims. This file must be imported before any SDK code:

polyfills.ts
import { Buffer } from 'buffer'

global.Buffer = Buffer

// Polyfill window object — React Native doesn't have one
let windowObj: any
if (typeof global !== 'undefined' && global.window) {
windowObj = global.window
} else if (typeof window !== 'undefined') {
windowObj = window
} else {
windowObj = {}
}

if (!windowObj.location) {
windowObj.location = {
hostname: 'mydapp.com',
href: 'https://mydapp.com',
}
}
if (typeof windowObj.addEventListener !== 'function') {
windowObj.addEventListener = () => {}
}
if (typeof windowObj.removeEventListener !== 'function') {
windowObj.removeEventListener = () => {}
}
if (typeof windowObj.dispatchEvent !== 'function') {
windowObj.dispatchEvent = () => true
}

if (typeof global !== 'undefined') {
global.window = windowObj
}

// Polyfill Event if missing
if (typeof global.Event === 'undefined') {
class EventPolyfill {
type: string
bubbles: boolean
cancelable: boolean
defaultPrevented = false
constructor(type: string, options?: EventInit) {
this.type = type
this.bubbles = options?.bubbles ?? false
this.cancelable = options?.cancelable ?? false
}
preventDefault() {
this.defaultPrevented = true
}
stopPropagation() {}
stopImmediatePropagation() {}
}
global.Event = EventPolyfill as any
windowObj.Event = EventPolyfill as any
}

// Polyfill CustomEvent if missing
if (typeof global.CustomEvent === 'undefined') {
const EventClass =
global.Event ||
class {
type: string
constructor(type: string) {
this.type = type
}
}
class CustomEventPolyfill extends (EventClass as any) {
detail: any
constructor(type: string, options?: CustomEventInit) {
super(type, options)
this.detail = options?.detail ?? null
}
}
global.CustomEvent = CustomEventPolyfill as any
windowObj.CustomEvent = CustomEventPolyfill as any
}
note

The Event and CustomEvent polyfills above are only required if you also use Wagmi, which dispatches DOM events. The @metamask/connect-* packages use eventemitter3 internally and don't need them.

tip

For detailed troubleshooting of polyfill issues, see React Native Metro polyfill issues.

Create the empty module stub used by the Metro config:

src/empty-module.js
module.exports = {}

4. Configure Metro

Metro cannot resolve Node.js built-in modules. Map them to React Native-compatible shims or the empty module stub:

metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config')
const path = require('path')

const emptyModule = path.resolve(__dirname, 'src/empty-module.js')

const config = {
resolver: {
extraNodeModules: {
stream: require.resolve('readable-stream'),
crypto: emptyModule,
http: emptyModule,
https: emptyModule,
net: emptyModule,
tls: emptyModule,
zlib: emptyModule,
os: emptyModule,
dns: emptyModule,
assert: emptyModule,
url: emptyModule,
path: emptyModule,
fs: emptyModule,
},
},
}

module.exports = mergeConfig(getDefaultConfig(__dirname), config)

5. Set up the entry file

The import order is critical. react-native-get-random-values must be the very first import, followed by the polyfills file, before any other code:

index.js or App.tsx (Bare RN) / app/_layout.tsx (Expo Router)
import 'react-native-get-random-values'
import './polyfills'
caution

If you import anything from @metamask/connect-evm before react-native-get-random-values, you will get crypto.getRandomValues is not a function.

6. Use MetaMask Connect EVM

Initialize the EVM client using createEVMClient. mobile.preferredOpenLink is required; it tells MetaMask Connect how to open deeplinks to the MetaMask Mobile app. Use the client to connect, sign, and send transactions:

import React, { useEffect, useRef, useState, useCallback } from 'react'
import { View, Text, TouchableOpacity, StyleSheet, Alert, Linking } from 'react-native'
import { createEVMClient } from '@metamask/connect-evm'

let clientPromise = null

function getClient() {
if (!clientPromise) {
clientPromise = createEVMClient({
dapp: {
name: 'My RN Dapp',
url: 'https://mydapp.com',
},
api: {
supportedNetworks: {
'0x1': 'https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
'0xaa36a7': 'https://sepolia.infura.io/v3/YOUR_INFURA_API_KEY',
},
},
ui: {
preferExtension: false,
},
mobile: {
preferredOpenLink: deeplink => Linking.openURL(deeplink),
},
})
}
return clientPromise
}

export default function App() {
const clientRef = useRef(null)
const [accounts, setAccounts] = useState([])
const [chainId, setChainId] = useState(null)
const [connecting, setConnecting] = useState(false)

useEffect(() => {
let mounted = true

async function init() {
const client = await getClient()
if (!mounted) return
clientRef.current = client

const provider = client.getProvider()
provider.on('accountsChanged', accs => {
if (mounted) setAccounts(accs)
})
provider.on('chainChanged', id => {
if (mounted) setChainId(id)
})
provider.on('disconnect', () => {
if (mounted) {
setAccounts([])
setChainId(null)
}
})
}

init()
return () => {
mounted = false
}
}, [])

const handleConnect = useCallback(async () => {
const client = clientRef.current
if (!client) return

setConnecting(true)
try {
const result = await client.connect({ chainIds: ['0x1'] })
setAccounts(result.accounts)
setChainId(result.chainId)
} catch (err) {
if (err.code === 4001) {
Alert.alert('Rejected', 'Connection was rejected.')
return
}
if (err.code === -32002) {
Alert.alert('Pending', 'Check MetaMask to approve the request.')
return
}
Alert.alert('Error', err.message ?? 'Connection failed')
} finally {
setConnecting(false)
}
}, [])

const handleDisconnect = useCallback(async () => {
const client = clientRef.current
if (!client) return
await client.disconnect()
setAccounts([])
setChainId(null)
}, [])

const isConnected = accounts.length > 0

return (
<View style={styles.container}>
{!isConnected ? (
<TouchableOpacity style={styles.button} onPress={handleConnect} disabled={connecting}>
<Text style={styles.buttonText}>{connecting ? 'Connecting...' : 'Connect MetaMask'}</Text>
</TouchableOpacity>
) : (
<View>
<Text style={styles.label}>Account: {accounts[0]}</Text>
<Text style={styles.label}>Chain: {chainId}</Text>
<TouchableOpacity style={styles.button} onPress={handleDisconnect}>
<Text style={styles.buttonText}>Disconnect</Text>
</TouchableOpacity>
</View>
)}
</View>
)
}

const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
button: { backgroundColor: '#037DD6', padding: 14, borderRadius: 8, marginVertical: 8 },
buttonText: { color: '#fff', fontSize: 16, textAlign: 'center' },
label: { fontSize: 14, marginVertical: 4 },
})

7. iOS configuration

Add the metamask URL scheme to your Info.plist so the app can open the MetaMask mobile app:

ios/MyProject/Info.plist
<key>LSApplicationQueriesSchemes</key>
<array>
<string>metamask</string>
</array>

8. Build and run

npx react-native run-android
npx react-native run-ios

Next steps