Peer dependencies are a bit of a gotcha in the JavaScript ecosystem, often leading to cryptic errors or unexpected version mismatches that feel like a black box.

Let’s see npm in action. Imagine you’re building a React component library, say my-react-ui, and you want it to work seamlessly with a specific version range of React.

// my-react-ui/package.json
{
  "name": "my-react-ui",
  "version": "1.0.0",
  "peerDependencies": {
    "react": "^17.0.0 || ^18.0.0",
    "react-dom": "^17.0.0 || ^18.0.0"
  },
  "dependencies": {
    "prop-types": "^15.8.1"
  }
}

Here, my-react-ui declares that it expects to be used in a project that already has react and react-dom installed, specifically versions 17.0.0 or higher, or 18.0.0 or higher. It doesn’t install these itself; it assumes the consuming project will provide them.

Now, consider a project that wants to use my-react-ui:

// my-app/package.json
{
  "name": "my-app",
  "version": "1.0.0",
  "dependencies": {
    "my-react-ui": "file:../my-react-ui", // Or a published version
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}

When npm install runs in my-app, npm sees that my-app has react and react-dom installed, and these versions satisfy the peerDependencies declared by my-react-ui. npm will then correctly link my-react-ui into the node_modules of my-app without attempting to install its own copies of react or react-dom.

The core problem peerDependencies solves is avoiding redundant, conflicting, or incompatible installations of a shared dependency. If my-react-ui were to declare react as a regular dependency, and my-app also declared react as a dependency, you could end up with two different versions of React installed side-by-side in node_modules. This is a recipe for disaster in libraries like React, where component instances and hooks are tied to specific module instances. peerDependencies forces the consuming application to be the "parent" that provides the required peer dependency.

The "parent" in this scenario is the application or library that uses your package. When you publish a package that has peerDependencies, you’re essentially saying, "I don’t bundle this because it’s expected to be provided by the environment where my package is installed." npm’s resolver checks if the actual installed versions of the peer dependencies in the consuming project satisfy the range specified. If they don’t, npm will warn you during installation.

This mechanism is crucial for libraries that integrate deeply with a host framework, like UI component libraries for React, Angular, or Vue, or plugins for larger systems. They rely on the host framework’s version to function correctly and avoid the "two Reacts" problem.

Here’s where it gets a bit nuanced: npm’s behavior around peer dependency conflicts has evolved. In older versions, npm might have just warned and continued, leaving you with a broken setup. With npm v7 and later, the installer is much stricter and will actively fail the installation if peer dependencies are not met, which is generally a better experience as it prevents you from running code that’s guaranteed to fail.

Consider this scenario: your my-react-ui package has react: "^17.0.0" as a peer dependency, but the consuming application my-app has react: "16.8.0".

// my-app/package.json
{
  "name": "my-app",
  "version": "1.0.0",
  "dependencies": {
    "my-react-ui": "file:../my-react-ui",
    "react": "16.8.0", // Older version
    "react-dom": "16.8.0"
  }
}

When npm install runs in my-app, npm will check my-react-ui’s peerDependencies. It will see that react: "16.8.0" does not satisfy ^17.0.0. Depending on your npm version, you’ll either get a prominent warning or a hard error preventing installation. The fix is to update my-app’s react and react-dom to a version that satisfies my-react-ui’s peer requirements, for example, ^17.0.0 or ^18.0.0.

The most surprising thing about peerDependencies is that they don’t install anything on their own, and their resolution is entirely dictated by the consuming project’s dependencies or devDependencies. You are not installing react for my-react-ui; you are installing react for your application, and my-react-ui is just stating its requirement for that application-provided react.

If you’re publishing a package that relies on a specific version of a framework or another major library that you don’t want to bundle, peerDependencies is your tool. It’s the contract between your library and the host environment.

The next hurdle you’ll often encounter is managing peerDependenciesMeta, which allows you to mark peer dependencies as optional.

Want structured learning?

Take the full Npm course →