How to fix NPM link duplicate dependencies issues
March 31, 2020 / 5 min read
Last Updated: March 31, 2020This post is about an issue that keeps coming on several of my projects involving third-party libraries linked locally with npm link: Duplicate dependencies. I ran into this issue multiple times, whether it was when working on a styled-components library even on some simple packages using only React as a peer dependency. I thought it could be worth writing about with an example it and explain why it happens and how I solve it.
Context of the problem
As an example, to illustrate the problem, we can consider a main React project called "myApp" bundled with Webpack. The app will be themed through emotion-theming and Emotion which use React Context to inject a theme object to be used by the entire app and any styled-components which are consumers of that “theme context”.
Now let’s consider a third-party package that will be consumed by our app, called "myLib". The "myLib" package has two important specs:
- It has React and Emotion as
peerDependencies
meaning that we want the "myLib" to use whatever version of React or Emotion the consumer app is using. - It has React and Emotion as
devDependencies
for development purposes. These are installed locally as we're actively working on this package. - It contains a set of Emotion styled-components such as Buttons, Dropdowns, Lists, that have colors, background colors, or fonts set by the theme of "myApp".
Here's an example of a component exported by "myLib" using a value of the theme provided by "myApp":
Example of styled component defined in myLib.
1import React from 'react';2import styled from '@emotion/styled';34const StyledButton = styled('button')`5background-color: ${(props) => props.theme.colors.blue};6border-color: ${(props) => props.theme.colors.black};7`;89export { StyledButton };
When developing "myLib" we don't want to have to publish a new version of the dependency every time we make a change to use it in "myApp". Thus we use npm link
to use the local version of our package.
Running this should technically work straight out of the box right? However, if we try to run this setup, nothing renders, and we end up with the following error:
1Cannot read property 'colors' of undefined
Even more bizarre, if we try to use the published version of "myLib" instead of the locally linked one, we do not get the error showcased above.
What is happening?
We know that the "production" setup (the one where we used the published version of "myLib") works: the theme is declared and passed through a React Context in "myApp" and is accessible in "myLib" as the components of this package are imported within that same context.
Corresponding code snippet from myApp rendering StyledButton
1import React from 'react';2import { ThemeProvider } from 'emotion-theming';3...4import { StyledButton } from 'myLib';56const theme = {7colors: {8black: "#202022",9blue: "#5184f9"10}11}1213const App = () => (14<ThemeProvider theme={theme}>15...16<StyledButton>Click Me!</StyledButton>17...18</ThemeProvider>19)
If we try to add a debugger or to run console.log(theme)
in "myLib" when npm linked we can see that it is not defined. The context value is somehow not accessible.
Troubleshooting this issue will require us to look outside of our app and into how the node_modules installed are architected. The following schema shows how "myApp" and "myLib" share their dependencies when "myLib" is installed:
1├─┬ myApp2└─┬ node_modules3└─┬── React4├── Emotion5└── myLib
The schema below, on the other hand, showcases how they share their dependencies when "myLib" is linked:
1├─┬ myApp2│ └─┬ node_modules3│ └─┬── React4│ ├── Emotion5│ └── myLib <- NPM linked package are "symlinked" to their local location6└─┬ myLib7└─┬ node_modules8├── React <- Local devDependency9└── Emotion <- Local devDependency
And that is where we can find the origin of our problem. In the second case, "myApp" and "myLib" use their own React and Emotion dependencies despite having specified these two as peer dependencies in the package.json
of "myLib".
Even if imported, when using React or Emotion within "myLib" we end up using the ones specified as development depencencies.
This means that the React context used by "myApp" is different than the one that "myLib" uses to get the theme variable. To make this work, we need them to use the same "instance of React".
The fix
To solve this issue, we have to take a look at Webpack. From its documentation, we can see that Webpack can change how modules are resolved when using the resolve.alias
option in a Webpack config. Adding such a field lets us declare how to resolve an arbitrary set of packages by providing a path for each of them (I often use this to override long or unpractical import statements).
The code snippet below showcases the solution to our problem by setting the react
and @emotion-core
dependency paths to the node_modules
folder of "myApp".
Example of using resolve.alias in a Webpack config file.
1// ... Webpack config ...2resolve: {3alias: {4react: path.resolve('./node_modules/react'),5@emotion/core: path.resolve('./node_modules/@emotion/core')6}
With this change, whether "myLib" is installed or linked to "myApp", it will have a similar dependency tree than the one described in schema 1 above. Setting this option properly might also fix some issues involving hooks described in the React docs which, in a setup similar than the one featured here, is often due to having multiple versions of React used within the same app.
We can now continue to develop our package without any duplicate dependencies issues 🎉. This is obviously one of many ways of fixing this problem, but it's the one that has fixed so many of my problems in the past, and I keep getting back to it every so often.
PS: In these weird times (I'm writing these words during the Covid-19 outbreak) remember to stay safe and stay home!
Liked this article? Share it with a friend on Bluesky or Twitter or support me to take on more ambitious projects to write about. Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I'll do my best to get back to you.
Have a wonderful day.
– Maxime
Dealing with dependencies when developing a package and using it through npm link.