Creating a NPM module we can import on any app or project to include a predefined set of React components is a very good way to achieve code reusability. If you are creating a visual component library or a state management container that you want to include on several apps making an NPM module out of it is a very good idea.

misschristi1972 @ Flickr

My main to-go choices of helper libraries to do this where: nwb (https://github.com/insin/nwb) & react-cdk (https://github.com/storybooks/react-cdk) Both good alternatives, but for the purpose of this article I will build one component library module from scratch. Of course, we will need some help.

NPM Module

To create the module lets make a folder “awesome-components” with a package.json file in it first.

$ npm init -y

We will get a standard initial package.json definition.

{

"name": "awesome-components",

"version": "1.0.0",

"description": "",

"main": "index.js",

"scripts": {

"test": "echo \"Error: no test specified\" && exit 1"

},

"keywords": [],

"author": "",

"license": "ISC"

}

Dependencies

Since we are creating a module we should use the peerDependencies section to specify what packages this library will need from the host application. In this case react (https://facebook.github.io/react/) and … styled-jsx (https://github.com/zeit/styled-jsx) for CSS-in-JS. Peer dependencies means that once out module is installed, it will use the dependencies packages from the host application and not try to install copies of them for himself.

{

...

"peerDependencies": {

"react": "^15.x",

"styled-jsx": "^1.x"

},

...

}

The other modules we need are: babel (cli, core, presets, plugins, etc) (https://babeljs.io/) to transpile all the React JSX code we will have and any other ES6 goodies that will make our task more fun. We also need, react, react-dom and styled-jsx added as dev dependencies. so…

$ yarn add --dev babel-cli babel-core babel-preset-es2015 babel-preset-react babel-preset-stage-1 react react-dom styled-jsx

After this, our package.json file might look something as this:

{

"name": "awesome-components",

"version": "1.0.0",

"description": "",

"main": "index.js",

"scripts": {

"test": "echo \"Error: no test specified\" && exit 1"

},

"keywords": [],

"author": "",

"license": "ISC",

"devDependencies": {

"babel-cli": "^6.24.1",

"babel-core": "^6.25.0",

"babel-preset-react": "^6.24.1",

"babel-preset-stage-1": "^6.24.1",

"react": "^15.6.1",

"react-dom": "^15.6.1",

"styled-jsx": "^1.0.10"

},

"peerDependencies": {

"react": "^15.x",

"styled-jsx": "^1.x"

}

}

Build Scripts

With the development dependencies in place, we need a simple build script to take our source code on ES6 with React JSX and transpile it to a more universal JavaScript version, that can then be imported on any app. This a simple one-liner.

Our source code will reside on a “src” folder and our built code will go into a “dist” folder.

{

"name": "awesome-components",

"version": "1.0.0",

"description": "",

"main": "dist/index.js",

"scripts": {

"clean-dist": "rm -rf ./dist",

"build": "yarn run clean-dist && NODE_ENV=production `yarn bin`/babel ./src --out-dir ./dist"

},

"keywords": [],

"author": "",

"license": "ISC",

"devDependencies": {

"babel-cli": "^6.24.1",

"babel-core": "^6.25.0",

"babel-preset-react": "^6.24.1",

"babel-preset-stage-1": "^6.24.1",

"react": "^15.6.1",

"react-dom": "^15.6.1",

"styled-jsx": "^1.0.10"

},

"peerDependencies": {

"react": "^15.x",

"styled-jsx": "^1.x"

}

}

With babel we need to specify a .babelrc file containing babel presets, plugins, etc we want to use. Usually you don’t need to specify a per-environment config but, we will here due to some little problems with styled-jsx that gets solved by having different configuration for building the module on dev and building for production/publishing.

{

"env": {

"production": {

"presets": ["es2015", "react", "stage-1"]

},

"development": {

"presets": ["es2015", "react", "stage-1"],

"plugins": ["styled-jsx/babel"]

}

}

}

And… we are ready to start creating some components.

Lets create a reusable button in a src/index.js file

There is nothing special about this button component. It has a couple props: title and onClick, it will show a button HTML tag, with a onClick handler, a title and some styling (blue background, white text, some padding, no border)

To build our code:

$ yarn build

yarn build v0.27.5

yarn run clean-dist && NODE_ENV=production `yarn bin`/babel ./src --out-dir ./dist

src/index.js -> dist/index.js

Done in 1.52s.

$

Notice the build script will transpile everything under the “src” folder, outputting to “dist” folder. And that’s pretty much it. At this point we have a working component library we can publish to a public/private NPM registry or simply use from Github directly (luckily yarn and npm support installing from git repos)

$ npm publish .

Be sure to have your npmjs.com credentials already logged.

Before doing this we will need a couple more essential files: .gitignore and .npmignore. The first allow us to avoid having files we don’t need on git. In this category we have the full node_modules folder. .npmignore on the other hand allow us to tell npm (or yarn) which files the final user of our module won’t need. Like “src” folder, .babelrc, test folders (if any), etc.

Oh yes, awesome-components@0.1.0 (at this point of time) is published on NPM (https://www.npmjs.com/package/awesome-components)

But, publishing out module on NPM to be able to see how the component looks is a slow process. Not very productive. We need some way to be able to showcase our components in development time. Enter Storybook (https://storybook.js.org/)

Storybook

Is a amazing tool, in the authors words Storybook is:

“The UI Development Environment You’ll ♥️ to use”

I know I love it.

Storybook will allow us to speed development of out components by adding an UI environment where we can showcase our components uses, different properties, all, in a fancy web UI, with hot-reloading and several other plugins that help us build faster better components.

Lets add a storybook to our module. We need first to install the storybook CLI package.

$ npm i -g @storybook/cli

We then run the right command on our module root folder.

$ getstorybook



getstorybook - the simplest way to add a storybook to your project. • Detecting project type. ✓

• Adding storybook support to your "React" library. ✓

• Preparing to install dependencies. ✓ yarn install v0.27.5

[1/4] Resolving packages...

[2/4] Fetching packages...

[3/4] Linking dependencies...

[4/4] Building fresh packages...

success Saved lockfile.

Done in 15.76s. • Installing dependencies. ✓ To run your storybook, type: yarn run storybook For more information visit: https://storybook.js.org $

This command will detect we are building a React component library, add the right devDependencies and 2 scripts to package.json.



"name": "awesome-components",

"version": "0.1.0",

"description": "Awesome components is a example of a React component library",

"main": "dist/index.js",

"scripts": {

"clean-dist": "rm -rf ./dist",

"build": "yarn run clean-dist && NODE_ENV=production `yarn bin`/babel ./src --out-dir ./dist",

"storybook": "start-storybook -p 6006",

"build-storybook": "build-storybook"

},

"keywords": [

"react",

"component",

"library",

"example"

],

"author": "Ernesto Freyre <

"license": "MIT",

"devDependencies": {

"babel-cli": "^6.24.1",

"babel-core": "^6.25.0",

"babel-preset-es2015": "^6.24.1",

"babel-preset-react": "^6.24.1",

"babel-preset-stage-1": "^6.24.1",

"react": "^15.6.1",

"react-dom": "^15.6.1",

"styled-jsx": "^1.0.10",

"@storybook/react": "^3.2.3"

},

"peerDependencies": {

"react": "^15.x",

"styled-jsx": "^1.x"

}

} "name": "awesome-components","version": "0.1.0","description": "Awesome components is a example of a React component library","main": "dist/index.js","scripts": {"clean-dist": "rm -rf ./dist","build": "yarn run clean-dist && NODE_ENV=production `yarn bin`/babel ./src --out-dir ./dist",},"keywords": ["react","component","library","example"],"author": "Ernesto Freyre < ernestofreyreg@gmail.com >","license": "MIT","devDependencies": {"babel-cli": "^6.24.1","babel-core": "^6.25.0","babel-preset-es2015": "^6.24.1","babel-preset-react": "^6.24.1","babel-preset-stage-1": "^6.24.1","react": "^15.6.1","react-dom": "^15.6.1","styled-jsx": "^1.0.10",},"peerDependencies": {"react": "^15.x","styled-jsx": "^1.x"

Also, 2 folders are added to the project. A .storybook folder with two files: config.js and addons.js. This are basically configurations for the Storybook. We won’t need in the scope of this post to change any of this files, but, I recommend you take a look was inside and how to make changes to your storybook.

The other folder is more interesting, is the stories folder. Stories in the Storybook context are basically hierarchically predefined views of components with different props, constructs, etc. By default will come up with a couple of examples of views. We will however modified to use our own Button component.

Notice we are using now our own Button component from the “src” folder.

To run the Storybook we do:

$ yarn run storybook

And open http://localhost:6006/

Storybook

We can keep adding components to our source code and at the same time create stories for them so we can visually see how are they looking on the Storybook, then, build and publish.

Why styled-jsx?

Styled-jsx is a simple and elegant solution to implement CSS-in-JS. Works really fine and is tightly integrated to my favorite React framework for server rendered applications Next.js (https://zeit.co/blog/next)

Is really nice to include a component library and not have to worry about styling, that otherwise we would have to track down and include on a build pipeline. This way only components actually on the page get their CSS transpiled and included on the page, even in SSR.

Testing

Of course, this is really important. Being only visual components IMHO simple snapshot testing might suffice. Luckily for us, the team at Storybook solved this problem already via a plugin StoryShots (https://github.com/storybooks/storybook/tree/master/addons/storyshots). We will need to install this plugin as a dev-dependency first. (we also need jest and react-test-renderer)

$ yarn add --dev @storybook/addon-storyshots jest react-test-renderer

Add a __test__/Snapshot.js file with the following content:

A “test” script on package.json:

{

...

"scripts": {

"test": "jest",

...

},

...

}

And a “test” section on our .babelrc file (identical to the development section):

{

"env": {

...

"test": {

"presets": ["es2015", "react", "stage-1"],

"plugins": ["styled-jsx/babel"]

}

}

}

When we run the tests the first time:

$ yarn test

yarn test v0.27.5

$ jest

PASS __test__/Snapshot.test.js

Storyshots

Button

✓ simple text (20ms)

✓ with some emoji (4ms) Snapshot Summary

› 2 snapshots written in 1 test suite. Test Suites: 1 passed, 1 total

Tests: 2 passed, 2 total

Snapshots: 2 added, 2 total

Time: 6.011s

Ran all test suites.

Done in 7.37s.

$

Since is the first time, snapshots will be created for all the Storybook stories we have so far and automatically make those tests pass.

If we change any of our components, HTML or CSS styling, test will fail. Lets try it:

Notice the change to a red background-color.

Tests again.

$ yarn test

yarn test v0.27.5

$ jest

FAIL __test__/Snapshot.test.js

● Storyshots › Button › simple text expect(value).toMatchSnapshot()



Received value does not match stored snapshot 1.



- Snapshot

+ Received



<button

className="Button"

- data-jsx={1276012340}

+ data-jsx={2949842903}

onClick={[Function]}

>

Hello Button

</button>



at Object.test (node_modules/@storybook/addon-storyshots/dist/test-bodies.js:27:18)

at Object.<anonymous> (node_modules/@storybook/addon-storyshots/dist/index.js:155:25)

at Promise.resolve.then.el (node_modules/p-map/index.js:42:16)

at process._tickCallback (internal/process/next_tick.js:103:7) ● Storyshots › Button › with some emoji expect(value).toMatchSnapshot()



Received value does not match stored snapshot 1.



- Snapshot

+ Received



<button

className="Button"

- data-jsx={1276012340}

+ data-jsx={2949842903}

onClick={[Function]}

>

😀 😎 👍 💯

</button>



at Object.test (node_modules/@storybook/addon-storyshots/dist/test-bodies.js:27:18)

at Object.<anonymous> (node_modules/@storybook/addon-storyshots/dist/index.js:155:25)

at Promise.resolve.then.el (node_modules/p-map/index.js:42:16)

at process._tickCallback (internal/process/next_tick.js:103:7) Storyshots

Button

✕ simple text (31ms)

✕ with some emoji (3ms) Snapshot Summary

› 2 snapshot tests failed in 1 test suite. Inspect your code changes or run with `yarn test -- -u` to update them. Test Suites: 1 failed, 1 total

Tests: 2 failed, 2 total

Snapshots: 2 failed, 2 total

Time: 5.85s

Ran all test suites.

error Command failed with exit code 1.

$

In other words, our tests fails, in this case because we changed the styling. But what if we want to keep that new changes to styling? To update the snapshots we can:

$ yarn test -- -u

This will run the snapshot testing rebuilding the snapshots and making all tests pass again.

Test coverage

Luckily (again) for us, Jest (https://facebook.github.io/jest/) includes support for generating coverage reports. We only need to add the parameter to the jest CLI.

{

...

"scripts": {

"test": "jest --coverage",

...

},

...

}

But first, lets change our Button component a little bit. Lets say the component will show a red button, instead of a blue one, if the title starts or ends with and “!” character.

Since we changed our styling and our HTML tags, the snapshots that we had will make this fail. We will run test updating the snapshots.

$ yarn test -- -u

The tests pass, but as you can see, coverage drop to a ~75% and we are not covering lines 5 and 9. Exactly the lines that returns true to the condition we expect to make the Button have a different background color.

So, lets add a couple of stories to cover this 2 cases.

And run tests again:

$ yarn test -- -u

yarn test v0.27.5

$ jest --coverage "-u"

PASS __test__/Snapshot.test.js

Storyshots

Button

✓ simple text (22ms)

✓ with some emoji (2ms)

✓ urgent text prefix (2ms)

✓ urgent text sufix (2ms) Snapshot Summary

› 2 snapshots written in 1 test suite. Test Suites: 1 passed, 1 total

Tests: 4 passed, 4 total

Snapshots: 2 added, 2 passed, 4 total

Time: 5.146s

Ran all test suites.

-----------|----------|----------|----------|----------|----------------|

File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |

-----------|----------|----------|----------|----------|----------------|

All files | 100 | 100 | 100 | 100 | |

src | 100 | 100 | 100 | 100 | |

index.js | 100 | 100 | 100 | 100 | |

stories | 100 | 100 | 100 | 100 | |

index.js | 100 | 100 | 100 | 100 | |

-----------|----------|----------|----------|----------|----------------|

Done in 9.37s.

Back to 100% again.

From here

What else we can do? A lot. Add a README file to your module, or even better build an static version of your module’s storybook and publish it to the module repo’s own Github page.

Also, take a look at all the amazing plugins Storybook provides (project/community)

Use a linter, standard (https://www.npmjs.com/package/standard) or xo (https://www.npmjs.com/package/xo)

And always use (and respect) semver (http://semver.org/) for your modules.