Solving the Dual Package Hazard with npm peerDependencies
Stop duplicating core frameworks in your node_modules. Learn how to use npm peerDependencies and peerDependenciesMeta to prevent the dual package hazard in library development.
20 Feb 2026, 03:53 UTC

The Singleton Problem in Node.js
When building a library—especially a plugin or a UI component kit—you often rely on a core framework like React, Vue, or a database driver. If you list that framework in your dependencies, npm installs a private copy of that framework inside your library's node_modules folder.
This creates a "dual package hazard." If the host application also has that framework installed, the project now contains two separate instances of the same library. For packages that rely on singletons, global state, or instanceof checks, this is catastrophic. You will see errors where a component is recognized as a different type than expected, or state updates in one instance fail to trigger renders in the other.
The solution is peerDependencies. Instead of saying "I need this version of the library," you are saying "I expect the person using my library to provide this version of the library."
How peerDependencies Shift Responsibility
By moving a package from dependencies to peerDependencies, you ensure that your library hooks into the version already present in the host application's root. This flattens the dependency tree and prevents duplication.
Since npm v7, the behavior has shifted: npm now attempts to automatically install peer dependencies if they are missing. However, it will throw a peer dependency conflict error if the host application's version is incompatible with the range you specified. This is a feature, not a bug—it alerts the developer that your library might break due to a version mismatch before the code even runs.
Handling Optional Peers
Sometimes your library supports a feature that is only available if a specific package is installed, but it isn't required for the library's core functionality. Using peerDependencies alone would force every user to install that package.
To solve this, use the peerDependenciesMeta field. This allows you to mark a dependency as optional, telling npm to ignore it during the installation process if the user hasn't explicitly added it to their project.
Implementation Example: A React Plugin
Imagine you are building a library called @example/chart-plugin that requires React to function. To avoid installing a second copy of React in the user's project, configure your package.json as follows:
{
"name": "@example/chart-plugin",
"version": "1.0.0",
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
}
Key configuration details:
- Version Range: Using
^17.0.0 || ^18.0.0allows the library to be compatible with multiple major versions, reducing the likelihood of installation conflicts for the end user. - Optional Peer:
react-domis marked as optional because the plugin might be used in a server-side rendering (SSR) environment wherereact-domisn't needed.
Verifying the Dependency Tree
To verify that your peer dependency is working and not duplicating, run the following command in the host project's terminal (requires npm installed globally):
npm list react
Expected Result: You should see a single version of React listed at the root. If you see a nested node_modules tree under your plugin containing another version of React, the peer dependency is not configured correctly.
Trade-offs and Limitations
The primary trade-off with peer dependencies is "dependency hell." If you are too restrictive with your version ranges (e.g., specifying exactly 18.2.0), you force your users to downgrade or upgrade their entire project just to use your library.
Additionally, because peer dependencies are not bundled with your library, you must handle the absence of optional dependencies in your code. You cannot simply import an optional peer; you must check for its existence at runtime to avoid Module not found errors:
try {
const ReactDom = require('react-dom');
// Use ReactDom functionality
} catch (e) {
// Fallback for environments without react-dom
}
Actionable Summary
When publishing a package, follow these rules of thumb:
- Use
dependenciesfor internal utilities that the user doesn't need to know about. - Use
peerDependenciesfor core frameworks, shared state managers, or any package that must remain a singleton. - Use
peerDependenciesMetafor features that are "nice to have" but not required. - Always use wide version ranges (using
||) to maximize compatibility across different host environments.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.