author-image

Stephen Kelly

Create a component library using Create React App, Storybook and Jest

Let’s create a reusable UI library that can be shared across multiple projects. React components are perfect for this.

“Components let you split the UI into independent, reusable pieces, and think about each piece in isolation.” – reactjs.org

We’ll build our UI following a Component-Driven Development (CDD) methodology.

Here at Fathom we use the create-react-app toolchain for React projects. It sets up your development environment so that you can use the latest JavaScript features and provides a nice developer experience.

Create react app requires you to have Node >= 8.10 and npm >= 5.6 on your machine. When creating this post I used Node 10.15 npm 6.4.1.

Let’s create a project, run:

# Pick a unique project name
npx create-react-app fathom-react-components
cd fathom-react-components
# Runs the frontend app on port 3000:
npm start

Create react app screen

Now that you have your project created, Let’s add Storybook and start to build components in isolation.

Setup Storybook using the automated command line tool. This command adds a set of boilerplate files for Storybook in your project:

cd fathom-react-components 
npx -p @storybook/cli sb init
# Starts the component explorer on port 9009:
npm run storybook

Storybook welcome screen

We have to make a change to the Storybook configuration setup .storybook/config.js.

By default Storybook looks for stories in a /stories directory; this tutorial uses a naming scheme that is similar to the .test.js naming scheme favoured by CRA for automated tests.

// .storybook/config.js

import { configure } from '@storybook/react';

const req = require.context('../src', true, /\.stories.js$/);

function loadStories() {
  req.keys().forEach(filename => req(filename));
}

configure(loadStories, module);

With our config updated we can now delete the /stories directory provided by storybook.

Let’s create a simple button component and it’s story file src/components/Button.js and src/components/Button.stories.js.

// src/components/Button.js

import React from 'react';

export default function Button({ text, onClick }) {
  return (
    <button onClick={onClick}>
      {text}
    </button>
  );
}
// src/components/Button.stories.js

import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';

import Button from './Button';

export const text = 'Hello Button';

export const actions = {
  onClick: action('onClick')
};

storiesOf('Button', module)
  .add('default', () => <Button text={text} {...actions} />);

Once the files have been created, restarting the Storybook server should show us our Button component.

Storybook button

Now that we have our component set up, let’s set up some Jest Snapshot testing.

With the Storyshots addon a snapshot test is created for each of the stories. Use it by adding a development dependency on the package:

npm i @storybook/addon-storyshots react-test-renderer require-context.macro --save-dev

And create a src/storybook.test.js file with the following inside:

// src/storybook.test.js

import initStoryshots from '@storybook/addon-storyshots';
initStoryshots();

You’ll also need to use a babel macro to ensure require.context runs in Jest (our test context). Install it with:

npm i babel-plugin-macros --save-dev

And enable it by adding a babel.config.js file in the root folder of your app (same level as package.json)

// babel.config.js
module.exports = function (api) {
    api.cache(true);
    
    const presets = [];
    const plugins = [ "macros" ];

    return {
      presets,
      plugins
    };
}

Then update .storybook/config.js to have:

// .storybook/config.js

import { configure } from '@storybook/react';
import requireContext from 'require-context.macro';

const req = requireContext('../src/components', true, /\.stories\.js$/);

function loadStories() {
  req.keys().forEach(filename => req(filename));
}

configure(loadStories, module);

Once the above is done, we can run npm test and see the following output:

Storybook button

How do we take our component and package it to be used in other projects?

First let’s add components/index.js this will be the entry point to our library and where we export our components:

// components/index.js

import Button from './Button';

export { 
    Button
};

Then we need to add some additional dev dependencies:

npm i cross-env @babel/cli @babel/preset-env @babel/preset-react --save-dev

And enable preset-env and preset-react by adding it to our presets array inside babel.config.js:

// babel.config.js

module.exports = function (api) {
    api.cache(true);
    
    const presets = [ "@babel/preset-env", "@babel/preset-react" ];
    const plugins = [ "macros" ];

    return {
      presets,
      plugins
    };
}

To avoid any React conflict issues you can move the following React dependencies to dev dependencies (As the app using this library will have React installed):

// package.json
"devDependencies": {
    "react": "^16.9.0",
    "react-dom": "^16.9.0",
    "react-scripts": "3.1.2"
    ...
}

To prepare for publishing, In your package.json add:

    "main": "dist/index.js",
    "private": false,
    "files": [ "dist", "README.md" ],
    "repository": {
        "type": "git",
        "url": "URL_OF_YOUR_REPOSITORY"
    }

We want Babel to compile our code from src/components and output it to the dist directory.

Let’s add the compile script:

// package.json

"clean": "rimraf dist",
"compile": "npm run clean && cross-env NODE_ENV=production babel src/components --out-dir dist --copy-files --ignore __tests__,spec.js,test.js,stories.js,__snapshots__"

Finally, To build your package run:

npm run compile

Now you have a package which you can publish to npm:

npm publish

When successful you can install your package via npm or yarn:

npm i fathom-react-components
yarn add fathom-react-components

And import into your app:

import { Button } from 'fathom-react-components';

Conclusion

Congrats, you have created a React component library with Storybook and Jest. This is a great start but there is more to think about.

How do you style your components? How do you set up your git repository to manage versioning? How do you automate deployment? etc.

These are questions we may explore in future posts, Check back with us so you don’t miss out.

SO WHAT DO YOU THINK ?

We love to talk about ideas, innovation, technogy and just about anything else. Really, we love to talk.

Contact fathom