Build your own reusable npm react package with Rollup

Rasmus Styrk
5 min readApr 11, 2021

Building your own reusable package is a great way of sharing code between your projects. For example, I have a collection of components like buttons, forms etc that I use in all my projects.

In this article I will show you how to build a simple HelloWorld component, test it and ship it to npm using a package bundler.

Why use Rollup?

Rollup is a module bundler. It’s simple, fast and has plugins for stuff like css, images etc. It handles modules that needs external (or “peerDependencies”). For example, if you use React you need to bundle your package with React as a peer dependencies because it cannot work with two instances of react on the same app.

I have used other bundlers as well and here is my thoughs:

  • Parceljs: It works, but not for modules and peerDepencies. You should not use it for creating npm packages
  • Microbundle: It works, but it does not handle scss/css or images, I will only recommend if your package is ment to be ultra small
  • Webpack: The mother of all, but most complex

Read more about rollup here

Hello World

The component we want to share between our projects is going to be a HelloWorld component that simply displays a text and a button to demostrate that everything works. When the user clicks the button a counter is going to be counted up.

Let us start out by creating a bunch of folders and emtpy files, then copy and paste the contets below.

$ mkdir my-components my-components/dist my-components/src my-components/src/components
$ touch my-components/src/index.tsx my-components/src/components/index.tsx my-components/src/components/hello_world.tsx
$ touch my-components/packages.json my-components/tsconfig.json my-components/rollup.config.js
$ cd my-components
// src/index.tsx
export * from "./components";
// src/components/index.tsx
export * from "./hello_world";
// src/components/hello_world.tsx
import React, { useState } from "react";

/**
* A simple HelloWorld component that renders a title and a button. Clicking the
* button counts up a number and rerenders the component
*/
export const HelloWorld = () => {
// Number of current clicks
var [clicks, setClicks] = useState<number>(1);

// Handler for when user clicks the button
const tick = () => {
setClicks(clicks + 1);
};

// Renders the view
return (
<div>
<h1>Hello World {clicks}</h1>
<button onClick={() => tick()}>Click me!</button>
</div>
);
};

This component is simple enough and is easy to test. Let us start by creating the right folders and files our code

Setting up Typescript, React and Rollup

To get everything to work we need to configure it using config files. Paste the following into the empty files that is left.

// packages.json
{
"name": "my-components",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "rollup -c",
"start": "rollup -c -w",
"release": "npm version patch && npm run build && npm publish",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@rollup/plugin-image": "^2.0.6",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"rollup": "^2.38.5",
"rollup-plugin-sass": "^1.2.2",
"rollup-plugin-typescript2": "^0.29.0",
"typescript": "^4.1.5"
},
"peerDependencies": {
"react": "^17.0.1",
"react-dom": "^17.0.1"
},
"files": ["dist"]
}
// tsconfig.json
{
"compilerOptions": {
"outDir": "dist",
"module": "esnext",
"target": "es5",
"lib": ["es6", "dom", "es2016", "es2017"],
"sourceMap": true,
"allowJs": false,
"jsx": "react",
"declaration": true,
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "example", "rollup.config.js"]
}
// rollup.config.js
import sass from "rollup-plugin-sass";
import typescript from "rollup-plugin-typescript2";
import image from "@rollup/plugin-image";
import pkg from "./package.json";export default {
input: "src/index.tsx",
output: [
{
file: pkg.main,
format: "cjs",
exports: "named",
sourcemap: true,
strict: false,
},
],
plugins: [
sass({ insert: true }),
typescript({ objectHashIgnoreUnknownHack: true }),
image(),
],
external: [
"react",
"react-dom",
"react-router-dom",
],
};

Install and Build

Now you can install everything and see if it compiles

$ npm install
$ npm run build

If it succeeds you should a message like this

created dist/index.js in 763ms

Testing your package

Good code needs testing. So lets start by creating our test-folders and files.

$ mkdir test test/components __mocks__
$ touch test/components/hello_world.test.tsx __mocks__/fileMock.js jest.config.js
// test/components/hello_world.test.tsx
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom/extend-expect";
import { HelloWorld } from "../../src/components/hello_world";
describe("<HelloWorld />", () => {
beforeEach(() => {
render(<HelloWorld />);
});
it("renders a title", () => {
expect(screen.getByRole("heading")).toHaveTextContent("Hello World 1");
});
it("counts up when button pressed", () => {
userEvent.click(screen.getByRole("button"));
expect(screen.getByRole("heading")).toHaveTextContent("Hello World 2");
});
});
// __mocks__/fileMock.js
module.exports = "test-file-stub";
// jest.config.js
module.exports = {
roots: ["<rootDir>"],
transform: {
"^.+\\.(ts|tsx)$": "ts-jest",
},
moduleDirectories: ["node_modules", "bower_components", "src"],
moduleNameMapper: {
"^.+\\.(css|less|scss)$": "babel-jest",
".+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"<rootDir>/__mocks__/fileMock.js",
},
};
// update scripts in packages.json to
"test": "jest --testTimeout=10000 --runInBand"

Install test dependencies

Okay, so now we got all the files and setup, then let us install jest and it’s dependencies

$ npm install --save-dev jest ts-jest @testing-library/react @testing-library/user-event @testing-library/jest-dom @testing-library/react

Run the test

To run the test you can invoke the test script that we provided in our package file

$ npm run test

When the test is done you should see the following

PASS  test/components/hello_world.test.tsx
<HelloWorld />
✓ renders a title (64 ms)
✓ counts up when button pressed (42 ms)

Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 3.063 s

Sweet. Everything works and the test pass!

Publishing to npm

So now we actually created our package and it is ready to be shipped to the npm registry. Head over to https://www.npmjs.com/ and create an account.

Once you have signed up, you can login on your command line

$ npm login

When your login is sucessfull you can publish your package to the registry

$ npm run release

This will bump the patch version number of the package, build it and publish it on the registry. When releasing to npm you will most likely see the following error

npm ERR! 403 403 Forbidden - PUT https://registry.npmjs.org/my-components - You do not have permission to publish "my-components". Are you logged in as the correct user?

This means that the package name is already taken or you simply do not have the access level to that name. To fix it, open packages.json and change the name field to what your package name should be. Okay, so now we just published a package to npm! Awesome!

Head over to https://www.npmjs.com/package/byteable-hello-world and see your package.

Adding a README.md

When you look at your package you need to supply at least a few instructions to whoever uses the packages.

// README.md
### Welcome to my byteable-hello-world
To install this package, follow this step$ npm install byteable-hello-world

Save it and run again npm run release. Now you should see a nice introduction page on your project page at npmjs.com

See the full example

That was quite a bit to cover and i hope you followed through. I have pushed the full code for the package to github

--

--

Rasmus Styrk

I work as a software developer with years of experience within the field of web, apps and server architecture.