TL;DR:
- The
peerDependencies
field is not for single copy of a package. - 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 asdependencies
instead. - The
peerDependencies
field is meant to warn you of any incompatibilities, so resolve unmet peer errors instead of ignoring them. - As the library author, make both your
dependencies
andpeerDependencies
ranges as wide as possible. - 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?
π‘ peerDependencies
field is designed to be used for declaring compatibility requirements between a library and other libraries used by the host app.
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:
βββ 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 .
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.
βββ 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.
βββ 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. 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:
- Should
lodash
be a peer dependency or dependency? - Should
tslib
be a peer dependency or dependency? - 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.
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 rule for bothdependencies
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
:
- dependency is below what peerDependencies require:
Just update the required dependency.
- dependency is above what peerDependencies require:
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
"overrides": {
"react": "$react"
}
For pnpm, there more choices:
- pnpm.peerDependencyRules.allowedVersions or some other related flag in package.json
{
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"react": "17"
}
}
}
}
- Pnpm installation hook: hooks.readPackage(pkg, context): pkg
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 for future possibility.
Other Things You Should Know About Peer Dependencies
- βmissing peerβ usually mean auto installation has been turned off, check out corresponding package manager doc for detail.
-
peer dependencies are transitive.
If
packageX
has a dependency onpackageY
, andpackageY
has a peer dependency onpackageZ
, thenpackageZ
is also a peer dependency ofpackageX
. 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 in pnpm monorepo, or a mismatch between babel plugins and babel version due to babel preset.
peer dependencies can be optional. See this page 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/about-semantic-versioning
https://nodejs.org/en/blog/npm/peer-dependencies
https://dev.to/arcanis/implicit-transitive-peer-dependencies-ed0
https://github.com/pnpm/pnpm/issues/4214
https://github.com/yarnpkg/berry/issues/4099
https://dev.to/arcanis/implicit-transitive-peer-dependencies-ed0
https://github.com/npm/rfcs/blob/main/implemented/0025-install-peer-deps.md
Top comments (2)
Such an excellent article! Thank you
very good article. thank you