Peer Dependencies in depth
Tags: webdev, frontend, packagemanager, beginners
Published: 2023-08-22 05:27:49


## TL;DR:

1. The `peerDependencies` field is not for single copy of a package.
2. Use `peerDependencies` when your host app needs a compatible version of another package to work with the library you provide. If your library doesn't have such constraints, list them as `dependencies` instead.
3. The `peerDependencies` field is meant to warn you of any incompatibilities, so resolve unmet peer errors instead of ignoring them.
4. As the library author, make both your  `dependencies` and `peerDependencies` ranges as wide as possible.
5. Do not overuse `peerDependencies`. Otherwise, library users will have difficulty resolving compatibility problems or may have to resort to ignoring them altogether.

## Introduction

If you have worked with node packages, you might have come across the term "peer dependency." 

However, I've noticed that some of my colleagues tend to think that ***peer dependencies are only for singletons*** or that ***they guarantee a single copy** in final bundles* so the final code size could be smaller. However, **this isn't quite accurate.**

In this blog post, we will explore what peer dependencies are, how they differ from dependencies, and how package managers like 'npm' work with them.  This post will also cover how to resolve peer dependencies conflict and best practice to de-duplicate package’s copy.

## What are Peer Dependencies?

<aside>
💡 `peerDependencies` field is designed to be used for declaring compatibility requirements between a library and other libraries used by the host app.

</aside>

In order to understand what peer dependencies are, we need to understand why they are needed in the first place and why `dependencies` are not enough.

So why `dependencies` are not  good enough? 

`Dependencies` work fine in terms of "compatibility". If a package you are building requires library `foo` version `1.0.0`, and your app depends on `foo@2.0.0`, your code will work fine as there are two copy of foo.

├── foo@2.0.0
└─┬ your-library@1.2.3
       └── foo@1.0.0

However, certain packages depend on other packages that must be installed by the user in order to function properly. If a package requires the presence of other packages, it is likely designed to be used with specific versions of those packages. If the installed versions are not compatible, an error should be thrown to alert the user to choose a compatible version instead of silently installing multiple versions as a sub-folder (as the `dependencies` field does). This ensures that the library can work correctly with the host package.

For example, consider the following structure:

```jsx
├── react-dom@16.0.0
└─┬ your-library@1.2.3
  └── react@17.0.0
```

Your library is a UI component library made with React. In order to work in a DOM environment, the `react` package needs to be rendered by `react-dom`, which should be installed in the root project using a method like `root.render(<App />)`. Depending on your code, your UI component library may have some constraints on the version of `react-dom`, even though `react-dom` is not required in your library's source code. For instance, your library uses React hooks, so [the `react-dom` library that mounts it must above version 16.8.0](https://legacy.reactjs.org/warnings/invalid-hook-call-warning.html) .

If only using `dependencies` field, the  `react-dom` would present inside your library folder. But this is futile because `react-dom@17.0.0` is not the package that the host app uses to render your `react` component.

```jsx
├── react-dom@16.0.0 // this is the package the host app use to provide 'render' API
└─┬ your-library@1.2.3
  └── react@17.0.0
  └── react-dom@17.0.0 // this is futile, since this is not the react-dom actually being used.
```

This is when `react-dom` should be a peer dependency.

Let’s look into another scenario, when `react` need to be a  a peer dependency.

```jsx
├── react-dom@16.0.0
├── react@16.0.0 
└─┬ your-library@1.2.3
  └── react@17.0.0 // incompatible with host app version
```

The project install your library has already has `react`  as dependency.`react` that your library uses needs to be the same version as the one installed by the user, so that [hooks can work properly](https://legacy.reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react). In order to use your library, the host app’s `react` version must be updated. **The user must be informed of this!!! Hence, the use of peer dependencies.**

## How Are Peer Dependencies Different From Dependencies?

As mentioned above, when deciding whether to list a package in `dependencies` or `peerDependencies`, **the only thing that should be taken into consideration is whether it imposes compatibility constraints on other libraries that the host app is using or must install.**

This means that the `peerDependencies` field is not for a singleton or single copy in the final bundle(We will explore how to achieve this later on).

Let's consider a question: should `react` and `react-dom` be listed as `dependencies` or `peerDependencies`?

The answer is: it depends.

Wait a second, did you just mention in the previous section that `react` and `react-dom` are peer dependencies? Yes, but that assumption was based on the idea that you are building a React-based UI library that provides React components for the host app to render. What if your library is a UI library that doesn't require `react-dom` to render and only adds rendered DOM to the outside DOM that the user provides? The use of React is relevant only to the library  and does not impose any constraints on other packages installed by the user. Furthermore, the host app does not need to install any other packages for the library to work. Therefore, `react` and `react-dom` are `dependencies` that matter only to you: the library author. You may change your implementation to `Vue` or `Svelte` at any time.

Here is a little quiz:

1. Should `lodash` be a peer dependency or dependency?
2. Should `tslib` be a peer dependency or dependency?
3. Should `rollup` be a peer dependency or dependency?

## What about multiple copies of package?

Does this mean that if we use the `dependencies` field, we may end up with multiple copies of the same package installed?

 Yes, if the versions are mismatched. This is also true for `peerDependencies`, except it won't resolve correctly by design.

As a matter of fact, `react` does support having multiple copies on the same page. This is important because having multiple copies ensures that all code works properly, even if the package version they rely on doesn't match. For example, if the host app is using a different version of `lodash` than the library is using, and there are breaking changes between these two versions, two copies are needed to ensure the code runs correctly.

Furthermore, [bundlers like webpack have some bundle rules that cause multiple copies even though there is only one copy of a package in node_modules tree.](https://medium.com/dailyjs/webpack-4-splitchunks-plugin-d9fbbe091fd0)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lhvcdtb8caz6ll7fsaf8.png)



Library has no way to ensure there is only one copy of package in final bundles or that the object inside library is singleton by modifying package json. The only correct way to create a singleton object is by adding a flag in the global scope.The number of copies in the node_modules folder is not determined by `dependencies` or `peerDependencies`, but by the package versions of the packages.

So make sure your packages follow the [semantic versioning](https://docs.npmjs.com/about-semantic-versioning) rule for both`dependencies` and `peerDependencies` , and make it as wide as possible like `"^1.0"` or `"1.x"`, instead of  `"~1.0.4"` or `"1.0.4"` .For packages with a version below `"1.0.0"`, the number in the middle represents the possibility of changes that break backward compatibility. If you know that such changes will not occur, you may use `"<1.0.0"` instead. Then the host app can use command like `dedupe`  to deduplicate dependencies with overlapping ranges.

## What should we do when conflicts occur with peer dependencies?

Should we ignore it?

If you have read the previous section, you now understand that peer dependency is a way to warn you that the packages you installed may not work with other packages you installed. Of course, you should resolve it instead of ignoring it.

Unfortunately, in npm versions 3 through 6, `peerDependencies` were completely ignored when building a package tree. Even today, both `npm` and `pnpm` have the `strict-peer-deps` flag set to false by default, which ignores indirect peers. This can cause packages to receive a peer dependency outside the range set in their package's `peerDependencies` object, potentially leading to bugs.

Using `-force` and `-legacy-peer-deps` is a shameful last resort, as it might break your code.

But how to resolve it exactly? 

Before diving into how to resolve it, check whether the library really should list the conflicting package as a peer dependency causing so much trouble.

If not, ask the author to change it into the `dependencies` field, and ask the author to make the version of the package as broad as possible.

If it should be listed in `peerDependencies`:

1. dependency is below what peerDependencies require:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hkvu5k884swwx5dmlene.png)



Just update the required dependency.

1. dependency is above what peerDependencies require:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d40bn5ms5wtm9v8i74eu.png)



If possible, ask the library author to update its peer dependencies. Alternatively, you can downgrade the dependencies of the host application.  Asking author to update might be the only solution in some cases when versions really are incompatible and you cannot afford to downgrade.

Sometimes a library may be unmaintained, but still working. In such cases, you may know that broadening the version of `peerDependencies` would not cause any compatibility issues. Alternatively, you may realize that the `peerDependencies` it requires should not even be a `peerDependencies` in the first place, but making the author understand this may take forever. Fortunately, your package manager provides a convenient way to avoid such hassle.

For npm above 8, you can use `overrides` field in package.json 

```json
  "overrides": {
    "react": "$react"
  }
```

For pnpm, there more choices:

1. [pnpm.peerDependencyRules.allowedVersions](https://pnpm.io/package_json#pnpmpeerdependencyrulesallowedversions) or some other related flag in package.json

```json
{
  "pnpm": {
    "peerDependencyRules": {
      "allowedVersions": {
        "react": "17"
      }
    }
  }
}
```

1. Pnpm installation hook: hooks.readPackage(pkg, context): pkg

```jsx
const peerDependencies = ['peerDependency1', 'peerDependency2'];
const { overrides } = rootPkg.pnpm;
function overridesPeerDependencies(pkg) {
    if (pkg.peerDependencies) {
        for (const dep of peerDependencies) {
            if (dep in pkg.peerDependencies) {
                pkg.peerDependencies[dep] = overrides[dep];
            }
        }
    }
}

module.exports = {
    hooks: {
        readPackage(pkg, _context) {
            // skipDeps(pkg);
            overridesPeerDependencies(pkg);
            return pkg;
        },
    },
};
```

However **`pnpm.overrides`**  isn’t working now.

For yarn, sadly there is no way to do it for the time being, you can track this [issue](https://github.com/yarnpkg/berry/issues/4099) for future possibility.

## Other Things You Should Know About Peer Dependencies

1. “missing peer” usually mean auto installation  has been turned off,  check out corresponding  package manager doc for detail.
2. peer dependencies are transitive.
    
    If `packageX` has a dependency on `packageY`, and `packageY` has a peer dependency on `packageZ`, then `packageZ` is also a peer dependency of `packageX`. This chain can continue until some package acts as the host.
    
    These can cause implicit problems, such as: [having two copies of exact same version packages](https://pnpm.io/how-peers-are-resolved) in pnpm monorepo, or [a mismatch between babel plugins and babel version due to babel preset](https://dev.to/arcanis/implicit-transitive-peer-dependencies-ed0).
    
3. peer dependencies can be optional. See [this page](https://docs.npmjs.com/cli/v9/configuring-npm/package-json#peerdependenciesmeta) for more details.

## Conclusion

In conclusion, peer dependencies are an essential part of creating and maintaining a library to ensure compatibility with other packages used in a host application. They are not meant for singleton or single copy in the final bundle, and their use should be carefully considered to avoid unnecessary conflicts and inconvenience for users. For users, it is crucial to resolve conflicts when they occur rather than ignoring them. For library authors, it is recommended to use peer dependencies only when necessary and be lenient **in version requirement. This post has become much longer that I intended to, so we will talk about how to resolve multiple  packages copies in future post.

Ref:

[https://docs.npmjs.com/cli/v9/configuring-npm/package-json/#peerdependencies](https://docs.npmjs.com/cli/v9/configuring-npm/package-json/#peerdependencies)

[https://docs.npmjs.com/about-semantic-versioning](https://docs.npmjs.com/about-semantic-versioning)

[https://nodejs.org/en/blog/npm/peer-dependencies](https://nodejs.org/en/blog/npm/peer-dependencies)

[https://dev.to/arcanis/implicit-transitive-peer-dependencies-ed0](https://dev.to/arcanis/implicit-transitive-peer-dependencies-ed0)

[https://stackoverflow.com/questions/64573177/unable-to-resolve-dependency-tree-error-when-installing-npm-packages](https://stackoverflow.com/questions/64573177/unable-to-resolve-dependency-tree-error-when-installing-npm-packages)

[https://github.com/pnpm/pnpm/issues/4214](https://github.com/pnpm/pnpm/issues/4214)

[https://github.com/yarnpkg/berry/issues/4099](https://github.com/yarnpkg/berry/issues/4099)

[https://dev.to/arcanis/implicit-transitive-peer-dependencies-ed0](https://dev.to/arcanis/implicit-transitive-peer-dependencies-ed0)

[https://github.com/npm/rfcs/blob/main/implemented/0025-install-peer-deps.md](https://github.com/npm/rfcs/blob/main/implemented/0025-install-peer-deps.md)