invertase / invertase/react-native-google-mobile-ads
[🐛] Missing react/react-native peerDependencies — duplicate React under pnpm monorepos (Invalid hook call in NativeAdView)
- Dominant language
- TypeScript
- Stars
- 1k
- Forks
- 229
- Avg merge
- 3d 5h
- Merged PRs (30d)
- 7
Description
## Summary
`react-native-google-mobile-ads@16.4.0` (current latest) declares **no `react` or `react-native` peer dependencies** — its only peer is an optional `expo`:
```json
"peerDependencies": { "expo": ">=47.0.0" },
"peerDependenciesMeta": { "expo": { "optional": true } },
"dependencies": { "@iabtcf/core": "^1.5.6", "use-deep-compare-effect": "^1.8.1" }
```
Under pnpm's isolated layout this is not cosmetic: because the library declares no `react` peer, pnpm creates **no `react` sibling link** in its virtual-store instance. The library's `require('react')` then falls through to pnpm's hidden fallback (`node_modules/.pnpm/node_modules/react`), which resolves to *whichever* React version pnpm hoisted there. In a monorepo containing more than one React (common: a React Native app plus a Next.js web app), that can be **a different React than the app's**, so Metro bundles two React copies and the first rendered `NativeAdView` crashes with:
```
Invalid hook call ... You might have more than one copy of React
Render Error: Cannot read property 'useRef' of null (NativeAdView.tsx:29)
```
## Evidence
pnpm workspace, RN `0.86.0`, app `react@19.2.3`, web app in the same workspace on `react@19.2.7`:
```
$ ls node_modules/.pnpm/react-native-google-mobile-ads@16.4.0_react@19.2.3/node_modules/
@iabtcf react-native-google-mobile-ads use-deep-compare-effect ← no react/react-native link
$ ls node_modules/.pnpm/react-native-pager-view@*/node_modules/ ← a lib that declares its peers
react react-native react-native-pager-view ← links present, correct copy
$ readlink node_modules/.pnpm/node_modules/react ← the fallback RNGMA actually hits
../react@19.2.7/node_modules/react ← the WEB app's React
```
Result: every other native library in the app resolves the app's `react@19.2.3`; this library alone resolves `react@19.2.7`, and the crash reproduces on the first ad fill (it hides behind no-fill sessions, which makes it look intermittent).
(The `_react@19.2.3` suffix on the instance directory comes from `use-deep-compare-effect`'s peer, not from this package's own manifest.)
## Suggested fix
Declare the peers the library actually requires, like other RN native modules:
```json
"peerDependencies": {
"react": "*",
"react-native": "*",
"expo": ">=47.0.0"
}
```
## Workaround for anyone else hitting this
Pin the singletons at the Metro resolver level (an `extraNodeModules` entry is NOT enough — it only catches resolutions that fail):
```js
// metro.config.js
resolver: {
resolveRequest: (context, moduleName, platform) => {
for (const [name, root] of [
['react', path.resolve(projectRoot, 'node_modules/react')],
['react-native', path.resolve(projectRoot, 'node_modules/react-native')],
]) {
if (moduleName === name) return context.resolveRequest(context, root, platform);
if (moduleName.startsWith(`${name}/`))
return context.resolveRequest(context, path.join(root, moduleName.slice(name.length + 1)), platform);
}
return context.resolveRequest(context, moduleName, platform);
},
}
```
## Environment
- react-native-google-mobile-ads: 16.4.0
- react-native: 0.86.0, react: 19.2.3 (New Architecture)
- pnpm 9.0.0 workspace (`node-linker=isolated`), monorepo also containing react 19.2.7 (Next.js)
- Android emulator + device, dev and release bundles resolve identically
Contributor guide
Research direction
Start with the package manifest and inspect its peerDependencies alongside the pnpm workspace reproduction described in the issue. Use the NativeAdView.tsx error and the package's virtual-store instance to verify resolution behavior; done means the package links the app's React and React Native copies without the invalid hook call.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react-native, typescript
- Domain
- mobile
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100