Build your own reusable npm react package with Rollup
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-worldTo 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