skip to content

Why should a React Native library, such as an in-house PDF viewer, declare react-native as a peerDependency rather than a regular dependency?

level: middleimportance: should knowfreq 35%

answer

  1. exactly one copy per app
  2. native code builds against the app's copy
  3. duplicate React is a runtime error
  4. the peer range states compatibility
  5. includesGeneratedCode pins the generating version

basics

~20 s

An app must contain exactly one react-native and one react. A peerDependency tells the package manager to use the app's copy and to warn when versions are incompatible, instead of installing a second copy inside the library.

solid answer

~40 s

A library's native code is compiled and code-generated against whatever `react-native` the app has: its headers, its Gradle plugin and its Codegen. If the library lists `react-native` under `dependencies`, a version mismatch installs a second copy in the library's `node_modules`, which Metro can bundle alongside the app's copy; duplicate React copies cause runtime errors and duplicate React Native versions are not supported. Declaring `react-native` and `react` in `peerDependencies` says "use the app's copy, and this is the range I support", and the package manager warns on a mismatch. The library keeps a pinned `react-native` in `devDependencies` for its own example app and tests. The same logic applies to native libraries it builds on: making them peers lets the app install them directly, where autolinking will find them.

code

json · 17 lines
json
{
  "name": "@my-org/pdf-viewer",
  "peerDependencies": {
    "react": "*",
    "react-native": ">=0.85.0"
  },
  "devDependencies": {
    "react": "19.2.3",
    "react-native": "0.87.1"
  },
  "codegenConfig": {
    "name": "RNPdfViewerSpec",
    "type": "all",
    "jsSrcsDir": "src",
    "android": {"javaPackageName": "com.myorg.pdfviewer"}
  }
}

go deeper

for a junior

Recall that a React Native library uses the app's copy of react-native and react, declared as peer dependencies.

for a middle

Explain how a private copy leads to duplicate React or React Native in one app, and why the library also pins versions in devDependencies.

for a senior

Choose peer ranges from what you actually test, treat native building blocks as peers so autolinking finds them, and align the range with includesGeneratedCode.

for a principal

Define the support window for an internal library shared by several apps on different React Native versions, and the upgrade contract between the library team and app teams.

## Three kinds of dependency A `package.json` can declare packages in three relevant places: | Field | Installed for consumers? | Meaning | |---|---|---| | `dependencies` | yes, possibly a private copy | "I need my own copy of this" | | `peerDependencies` | no; the app provides it | "I run inside a host that provides this, in this range" | | `devDependencies` | no | "I need this only to develop and test the package" | For a React Native library, `react-native` and `react` are the **host**: the library is code that runs inside an app that already has them. ## Why a private copy breaks things Suppose three apps share an in-house PDF-viewer package and the package lists `react-native: 0.86.3` under `dependencies`. An app on 0.87.1 installs it and the package manager, unable to satisfy both, puts a second `react-native` inside the library's own `node_modules`. Now: - **Metro may bundle two copies** of React Native's JavaScript, and if two copies of `react` end up in one app, hooks and context break with runtime errors. - **Native code exists once**: the binary contains one React Native native runtime, built from the app's version, so JavaScript from a second, different version would run against native code it was not written for. - **Codegen** runs in the app's build using the app's React Native, so the library's own copy only adds confusion. The Expo monorepo guide states the rule plainly: duplicate React Native versions in one monorepo are not supported, and duplicate React versions in one app cause runtime errors. ## What a peer dependency gives you 1. **One copy.** The library uses the app's `react-native`. 2. **A stated range.** `"react-native": ">=0.85.0"` documents what the library was built and tested against. 3. **A warning, not a silent duplicate.** When an app falls outside the range, the package manager reports a peer conflict at install time. The library still needs React Native to build its example app and run tests, so it pins a version in `devDependencies` as well. ## Native libraries your library needs If the PDF viewer builds on another native library, making it a peer dependency matters for a second reason: the community CLI autolinks the **app's** declared dependencies, so a native library that sits only inside your package's `dependencies` may never be linked. As a peer, the app installs it directly and autolinking sees it. Expo Autolinking resolves transitive dependencies, but a peer still keeps one copy per app. ## includesGeneratedCode and version ranges Normally a library ships only its spec files, and the **app** runs Codegen at build time with its own React Native version. Setting `"includesGeneratedCode": true` in the library's `codegenConfig` changes that: the library runs Codegen itself (for example with `npx @react-native-community/cli codegen`) and publishes the generated native code. - **Benefits:** the app no longer needs to generate anything for it, the implementation always matches its generated interfaces, and the native part can be shipped prebuilt. - **Drawback:** the generated code comes from the library's React Native version, and the React Native docs warn it may not work in apps on an **older** version. So a library that ships generated code should set its peer range's lower bound to the React Native version it generated with. The docs also warn against committing generated code casually for the same reason and point to `peerDependencies` as the way to restrict compatibility. ## Common mistakes - Declaring `react-native: "*"` as a peer: it promises support for versions nobody tested. - Pinning an exact peer version, which turns every app upgrade into a library release. - Forgetting `react` as a peer, so a second React slips in through the library. - Bundling another native library privately, where the app's autolinking may not see it. - Shipping generated code without raising the peer floor to the generating version. ## A practical package.json The library lists `react` and `react-native` as peers with a floor it has tested, pins exact versions under `devDependencies` for its example app, and keeps pure-JavaScript helpers in `dependencies`, where a private copy harms nothing.

  • What does includesGeneratedCode change for a library?
    With `includesGeneratedCode: true` in `codegenConfig`, the library runs Codegen itself and ships the generated native code, so apps do not generate it at build time and the native part can be prebuilt. The cost is that the code reflects the library's React Native version and may not work in apps on older versions, so the peer range's floor must match.
  • How do you find a duplicate react-native in an app?
    Ask the package manager why it is installed, for example `npm why react-native`, `yarn why react-native` or `pnpm why react-native`, and look for two versions in the output. Then widen the library's peer range, align the app's version, or add a resolution so a single copy is installed.

saying these in an interview costs you the question

  • Listing react-native under dependencies guarantees the library's tested version is used
  • Peer dependencies are installed automatically as private copies inside the library
  • Two copies of react in one app are harmless because Metro deduplicates them
  • A library does not need react-native in devDependencies if it is a peer
  • Shipping generated code makes a library compatible with every React Native version