Back to System Design
System Design
medium
mid

When and how should you use peerDependencies in a JavaScript package?

A peerDependency declares 'I need this, but the host app provides it' — used for shared singletons (React, a router) where multiple copies break things. The host installs one shared version; you don't bundle it. Use a wide version range; the host is responsible for satisfying it.

4 min read·~6 min to think through

A peer dependency says: "my package needs this, but the consuming application must provide it — I won't bring my own copy."

The three dependency types

  • dependencies — your package needs it and brings its own copy. Installed automatically, nested under your package.
  • devDependencies — needed only to build/test your package, not at runtime for consumers.
  • peerDependencies — your package needs it at runtime, but expects the host app to install it and share that single instance. You declare a compatible version range; you do not bundle it.

When to use peerDependencies

Use them for libraries that must be a single shared instance across the whole app:

  • React / ReactDOM — the #1 case. Two copies of React → hooks break, context doesn't cross the boundary, instanceof checks fail, "Invalid hook call" errors. A React component library must peer-depend on React, not bundle it.
  • A router, a state library, a styling engine — anything stateful or relying on shared module-level state or singletons.
  • Plugins/extensions that must use the same instance as the thing they extend (e.g. an ESLint plugin peer-depends on ESLint).

The general rule: if multiple versions/copies would break things, or it's the host's environment, make it a peer dependency.

How to use them

json
{
  "peerDependencies": {
    "react": ">=17.0.0",
    "react-dom": ">=17.0.0"
  },
  "peerDependenciesMeta": {
    "react-dom": { "optional": true }
  },
  "devDependencies": {
    "react": "^18.0.0"   // also a devDep so YOU can build/test against it
  }
}
  • Use a wide range (>=17, or ^17 || ^18) — you want to be compatible with whatever the host already uses. Too narrow and you cause version conflicts.
  • Also list it as a devDependency so your own build and tests have it.
  • peerDependenciesMeta marks some peers optional.
  • Modern npm (v7+) auto-installs missing peers; older npm/yarn just warn — so document the requirement.

The benefits

  • One shared instance — no broken hooks/context, no instanceof failures.
  • No duplication — React isn't bundled three times across three of your libraries.
  • Host controls the version — the app picks its React version once.

The framing

"A peer dependency means 'I need this, but the host app provides the single shared copy — I won't bundle my own.' You use it for things that must be one instance across the app: React is the canonical case — two copies break hooks and context. Also routers, state libraries, plugin hosts. You declare a wide version range so you're compatible with whatever the consumer already has, also list it as a devDependency so you can build and test, and document it since older package managers only warn rather than install. The payoff is a single shared instance and no duplication."

Follow-up questions

  • What breaks if a React component library bundles its own React?
  • Why should the peer dependency version range be wide?
  • Why also list a peer dependency as a devDependency?
  • How do modern vs older package managers handle missing peers?

Common mistakes

  • Putting React in dependencies instead of peerDependencies — duplicate React, broken hooks.
  • Too narrow a peer version range, causing conflicts.
  • Forgetting to also add it as a devDependency for your own build/tests.
  • Not documenting the peer requirement.

Performance considerations

  • Peer dependencies prevent the same library being bundled multiple times across your packages — directly reducing the consumer's final bundle size and avoiding duplicate runtime state.

Edge cases

  • Host app has a version outside your declared range.
  • Optional peer dependencies (peerDependenciesMeta).
  • Multiple of your libraries with mismatched peer ranges.
  • Monorepos with hoisting affecting peer resolution.

Real-world examples

  • Every React component library peer-depending on react/react-dom.
  • ESLint plugins peer-depending on ESLint; Babel plugins on @babel/core.

Senior engineer discussion

Seniors explain peer deps as host-provided shared singletons, give React as the canonical break-if-duplicated case, prescribe wide ranges + a matching devDependency, and note package-manager behavior differences.

Related questions