Common pitfalls: Creating a custom NPM package with React and TypeScript

This article is written by Vojtech Rinik, lead frontend engineer at Thinknum. We started using React in 2014, and currently it powers most of our UI. We're at version 16.8 now, enjoying the latest features in this recent release. If you're in NYC and enjoy writing React code, check out our open positions.

I recently decided to create a private NPM package for some of the UI we use in our apps. The goal was simple. I wanted to install and start using the package with one line of code: yarn add rainbow-buttons. ("Rainbow" buttons being an example.)

I had some clear requirements for this package:

  • It should be written in TypeScript, like the rest of our codebase
  • It should contain its type declarations, so that we wouldn't have to install separate package from @types/rainbow-buttons.
  • It should contain React components. We'll want to do <RainbowButton>Hello!</RainbowButton>.
  • It should contain it's own CSS styles written in Sass. Those should be imported automatically when I run import {RainbowButton} from "rainbow-buttons".

Unfortunately, it wasn't straightforward at all. I came across quite a few problems. In this blog post, I'd like to go through these problems, explain what caused them and how to address them.

Pitfall 1: Don't forget to use peer dependencies

At my first attempt, I added all of package's dependencies into its regular, production dependencies list. This was a bad idea of course.

What happened was that my React dependency, for example, was installed by my package, but also by the host project. So now I had two instances of React, possibly at two different versions.

This is kinda obvious now, but that's why NPM/Yarn has the concept of peer dependencies. Your package will specify which dependencies it needs, and the host project is responsible for installing them.

Some of peer dependencies are set to "*", any version, and some are locked at a particular version.c

Pitfall 2: Don't use WebPack, use Rollup

I was seeing a lot of errors when just trying to build the package with WebPack. One of the problems was with peer dependencies. They were defined in peerDependencies section of package.json, but as soon as I removed the actual files from node_modules/, the build broke. So to fix that issue, I had to add them also to devDependencies, because Yarn won't install peer dependencies on regular yarn install, only dev and prod dependencies.

My package.json was quickly becoming a mess. I had to keep @types packages in dev dependencies, and regular packages in both dev and peer dependencies. Go figure.

There were more strange errors which I still don't know how to resolve. Most likely version mismatches.

But somehow, everything works fine once I switched to Rollup. If you haven't used it yet, it's an alternative packaging tool which is primarily used in libraries. It's used by React, D3.js, and many more popular open-source packages. There is a great article explaining the main differences, and a great article explaining how to use it with TypeScript.

I ended up with fairly simple config file:

import typescript from 'rollup-plugin-typescript2';
import pkg from './package.json';
import postcss from 'rollup-plugin-postcss';

export default {
  input: 'src/index.ts',
  output: [
    {
      file: pkg.main,
      format: 'cjs',
    },
    {
      file: pkg.module,
      format: 'es',
    },
  ],
  external: [
    ...Object.keys(pkg.dependencies || {}),
    ...Object.keys(pkg.peerDependencies || {}),
  ],
plugins: [
    typescript({
      typescript: require('typescript'),
    }),
    postcss({
      modules: true,
      namedExports: true
    })
  ],
}

The ES module makes sense when you pair it with module config in your package.json:

"main": "dist/index.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",

You can see a few more nifty things going on in that config file:

  • It's automatically defining all peer dependencies AND dependencies as external. The resulting package should only contain the package files.
  • It's taking care of CSS compilation including generating modules to be used as import * as styles from "./styles.scss"

But here's the important thing: Rollup doesn't need your peer dependencies to be actually installed in node_modules/. It just needs your @types/* packages, which are installed as dev dependencies, so they would be present in node_modules/ at all times.

It allowed me to clean up my package.json with double dev and peer entries for my peer dependencies. The package.json file can now be this simple:

"devDependencies": {
  "@types/react": "16.4.6",
  "@types/react-dom": "16.8.3",
  // ...
},
"peerDependencies": {
  "react": "*",
  "react-dom": "*",
  // ...
}

Our package relies on types packages for some 3rd party libraries, such as @types/react or @types/recompose. These are defined as dev dependencies in package.json.

With this setup, compiling the main project would return a bunch of Duplicate identifier errors all over the place:

Many more errors not shown here

After some Googling, I figured out the problem is with TypeScript compiler. These type definitions now exist in project's node_modules, but also in package's node_modules. Somehow, TypeScript is not able to deduplicate these type definitions when symlinking is used. And yarn link relies on symlinking.

This can be resolved with some smart configuration in your main project, but my favorite solution is to use yalc. It's a simple tool that simulates publishing to NPM by actually copying the files.

The usage is simple:

# In your package:
yalc publish

# In your main project:
yalc add rainbow-buttons

# In your package, after a rebuild:
yalc push

The last command is the most interesting: When you run yalc push, it will go to each project where it's used, and automatically copy the newest files over.

So I end up running this each time I change any code in my package:

yarn build && yalc push

You can read more about the issue in this blog post.

Conclusion & source code

That's it, a summary of some real-world problems you can come across when developing a non-trivial TypeScript package for React.

If you're interested in source code of this particular package, we've actually published it as open-source library called react-tour. Feel free to work through the configuration files if anything's not making sense.