Skip to main content

React Native Metro polyfill issues

React Native uses the Metro bundler, which cannot resolve Node.js built-in modules. MetaMask Connect packages and their dependencies reference modules like stream, crypto, buffer, and http, and also rely on browser globals (window, Event, CustomEvent) that do not exist in React Native.

This guide walks through the required polyfills and Metro configuration.

Expo-managed workflow

Polyfilling is not supported with the "Expo Go" app. It is compatible only with Custom Dev Client and Expo Application Services (EAS) builds. Prebuild your Expo app to generate native code before proceeding.

Steps

1. Install required packages

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

react-native-get-random-values provides crypto.getRandomValues, which MetaMask Connect requires. readable-stream provides a stream shim for Metro. buffer provides the Buffer global. @react-native-async-storage/async-storage is needed for session persistence.

2. Configure Metro

Map Node.js built-in modules to React Native-compatible shims or an empty module stub. Create an empty module file first:

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

Then update your Metro config:

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);

3. Create the polyfills file

Create polyfills.ts at the project root (or src/polyfills.ts) with all required global shims:

polyfills.ts
import { Buffer } from "buffer";

global.Buffer = Buffer;

// Polyfill window — React Native doesn't have a browser window object
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;
}

Step 4: Set up the entry file import order

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

index.js
import "react-native-get-random-values"; // Must be first
import "./polyfills"; // Must be second

import { AppRegistry } from "react-native";
import App from "./App";
import { name as appName } from "./app.json";

AppRegistry.registerComponent(appName, () => App);

Common errors and solutions

crypto.getRandomValues is not a function

Cause: react-native-get-random-values was not imported before MetaMask Connect.

Fix: Ensure import 'react-native-get-random-values' is the very first import in your entry file, before any MetaMask Connect imports or polyfills.

Buffer is not defined

Cause: The Buffer polyfill was not loaded before MetaMask Connect accessed it.

Fix: Ensure global.Buffer = Buffer is set in your polyfills file, and the polyfills file is imported immediately after react-native-get-random-values.

Cannot resolve module 'stream' (or crypto, http, etc.)

Cause: Metro does not know how to resolve Node.js built-in modules.

Fix: Add extraNodeModules to your metro.config.js as shown in Step 2. Map stream to readable-stream and stub the rest with the empty module.

Event is not defined or CustomEvent is not defined

Cause: React Native does not provide browser Event/CustomEvent classes that MetaMask Connect's internal event system requires.

Fix: Ensure the Event and CustomEvent polyfills are included in your polyfills file as shown in Step 3.

Expo Go not working

Cause: Polyfilling native modules is not supported with Expo Go.

Fix: Use a Custom Dev Client or EAS builds. Run npx expo prebuild before building.

Next steps