First Published 19 October 2019
(Last Updated 12 March 2021)
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 NodeJS and npm installed. When creating this post I used Node 14.15.5
npm 6.14.11
. At time of writing, I am using create-react-app version 4.0.3
React version 17.0.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
Now that you have your project created, Let’s add Storybook and start to build components in isolation. At time of
writing, storybook is at version 6.1.21
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 adds its example stories to a /stories
folder. In this tutorial, we do not adopt that convention and simply rely on the .stories.js
naming scheme.
Should you wish to adopt a different convention for how your stories are
named or where they are stored, the configuration file .storybook/main.js
allows you to control the conventions used.
You can 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
.
import React from 'react';
import PropTypes from 'prop-types';
export default function Button({ label, backgroundColor, onClick }) {
return (
<button onClick={onClick}
style={backgroundColor && { backgroundColor }}>
{label}
</button>
);
}
Button.propTypes = {
backgroundColor: PropTypes.string,
label: PropTypes.string.isRequired,
onClick: PropTypes.func,
};
Button.defaultProps = {
backgroundColor: null,
onClick: undefined,
};
// src/components/Button.stories.js
import React from 'react';
import Button from './Button';
export default {
title: 'Example/Button',
component: Button,
argTypes: {
backgroundColor: { control: 'color' },
},
};
const Template = (args) => <Button {...args} />;
export const Default = Template.bind({});
Default.args = {
label: 'My Button',
};
Once the files have been created, restarting the Storybook server should show us our Button component.
Snapshot Tests
Snapshot testing is a common type of regression testing, where an image of a UI or UI component is captured as a reference and then when code changes are made a new capture is compared to the reference. Storyshots is an official Storybook addon for snapshot testing. Now that we have our component set up, let’s set up some Snapshot testing using Storyshot and Jest. Note that Create React App will have already set up Jest.
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
And create a src/storybook.test.js
file with the following inside:
// src/storybook.test.js
import initStoryshots from '@storybook/addon-storyshots';
initStoryshots();
Once the above is done, we can run npm test
and see the following output:
Packaging Our Components
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 peer dependencies, as the app using this library will have React installed. If you need a refresher on peer dependencies, see this Fathom blog post.
// package.json
"peerDependencies": {
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-scripts": "4.0.3",
...
}
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.