Building a React Component Library | Part 1

Written by _alanbsmith | Published 2017/06/03
Tech Story Tags: nodejs | open-source | react | css | javascript

TLDRvia the TL;DR App

a tutorial for publishing your own component library to npm

Introduction

The purpose of this series is to walk through creating a small component library so you can learn how to build your own.

This is Part 1 of this series. This section will mostly focus on setting up our module and file structure. But we’ll end by building an example component! 🎉

If you’re looking for Part 2, look here!

Assumptions

I’m not expecting you’ve published a module to npm before, but we’ll start with a few basic assumptions:

  • Node & npm are installed
  • Basic proficiency with Node & npm
  • Git is installed
  • Basic proficiency with Git & Github
  • Fluency in JavaScript & ES6
  • Basic proficiency with CSS
  • Basic proficiency with React

It would also be helpful to be familiar with styled-components and Babel, but it’s not required.

Up & Running

Setting up Git and npm

First we’ll create a new directory. I’m going to name mine: component-lib.

$ mkdir component-lib

Now we’ll setup Git and GitHub.

$ cd component-lib
$ git init

NOTE: You’ll need to add a repo on GitHub first.

git remote add origin git@github.com:alanbsmith/component-lib.git

Cool. Now we’re ready to setup npm. If you already have an account on npm, you can just run npm login.

Otherwise we’ll need to set up an account for you. We’ll set an author name, email (which is public), and url.

$ npm set init.author.name "Your Name"
$ npm set init.author.email "you@example.com"
$ npm set init.author.url "http://yourblog.com"

$ npm adduser

Now we can run npm init and set up our project accordingly. Most of the default settings are fine for now. The few that you might pay attention to are the version, description, and entry point. And remember you can update all of these later.

$ npm init
name: (component-lib)
version: (1.0.0) 0.1.0
description: an example component library built with React!
entry point: (index.js) build/index.js
test command:
git repository:
keywords:
license: (ISC)
About to write to /Users/alanbsmith/personal-projects/trash/package.json:

{
  "name": "component-lib",
  "version": "0.1.0",
  "description": "an example component library built with React!",
  "main": "build/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Alan Smith <alan.smith@example.com> (https://github.com/alanbsmith)",
  "license": "ISC"
}

Is this ok? (yes)

I typically like to version projects initially at 0.1.0 instead of 1.0.0, which is the default. I also like to add a brief description of what this module is or does. We also need to change the entry point to build/index.js (We’ll explain this more later). Notice that your author and GitHub information was added automatically. This helps connect your npm docs with GitHub, and makes it really easy for your users to learn more about your module.

Awesome. Now we’re ready to add our files and directories.

Adding Files and Directories

This section is pretty dry, so I’ll be terse for most of it and touch down for the important bits.

$ mkdir lib
$ touch .babelrc .eslintrc .gitignore .npmignore CODE_OF_CONDUCT.md README.md
$ touch lib/index.js
$ mkdir lib/components lib/elements lib/styles

Great. Now you have the basic file structure you’ll need. A few points:

  • .babelrc will contain some helpful presets for our compilation
  • .eslintrc will contain our linter config
  • .gitignore and .npmignore ignores files from Git and npm respectively
  • CODE_OF_CONDUCT.md is super important for Open Source work, and I can’t stress that enough. But don’t add it lightly. If you have a COC, you should also be willing to enforce it.
  • README.md is also super important. This will be our main way to communicate with our Open Source community.
  • /lib is where all our components and styles will live.

Wait, what about tests?

We’ll get there. I promise. But I didn’t want to cram test setup into this section as well.

Adding our Initial Dependencies

This section is also pretty dry. I’ll point out the important parts as we go and skip the rest. I’m using $ npm install for these commands, but if you prefer yarn, go for it.

🚨 The--save-dev at the end is really important! 🚨

We don’t need our users to haul all of this down with our module just to use our compoents. These libs are only used for our local development.

$ npm install babel-cli babel-core babel-eslint babel-preset-es2015 babel-preset-react eslint eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-watch polished prop-types react react-dom styled-components --save-dev

And now we wait…

Whew. That was a lot. Let’s hit the high points. If you’re familar with React, things like react, react-dom, babel (and the presets), and prop-types will look pretty familiar. And if you’re familiar with eslint, the plugins probably look familiar as well.

The potential newcomers are styled-components and polished. As mentioned earlier, we’ll be using the styled-components lib to create our library. And you can think of Polished as a little add-on that gives us some nice Sass functionality. It’s not technically essential, but I thought it would be cool to introduce it here.

Cool. Now that we have that setup, we’ll fill in the little files we created along the way.

A Quick Note:

Below I’m going to use the term “transpile” a bit. The word is a mashup between “transform” and “compile.” If that’s unfamiliar to you, you can mostly equate it with “compile” and don’t worry too much about the details. If you want to know more though, you can read a great article here.

Initial File Setup

.babelrc

{
  "presets": ["es2015", "react"]
}

.eslintrc

People have lots of opinions about their linters. If you want to use your own, go for it. If you don’t have strong feelings, here’s a nice one.

{
  root: true,
  parser: 'babel-eslint',
  plugins: [/*'import', */'jsx-a11y', 'react'],

env: {
    browser: true,
    commonjs: true,
    es6: true,
    jest: true,
    node: true
  },

parserOptions: {
    ecmaVersion: 6,
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true,
      generators: true,
      experimentalObjectRestSpread: true
    }
  },

settings: {
    'import/ignore': [
      'node_modules',
      '\\.(json|css|jpg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm)$',
    ],
    'import/extensions': ['.js'],
    'import/resolver': {
      node: {
        extensions: ['.js', '.json']
      }
    }
  },

rules: {
    // http://eslint.org/docs/rules/
    'array-callback-return': 'warn',
    'camelcase': 'warn',
    'curly': 'warn',
    'default-case': ['warn', { commentPattern: '^no default$' }],
    'dot-location': ['warn', 'property'],
    'eol-last': 'warn',
    'eqeqeq': ['warn', 'always'],
    'indent': ['warn', 2, { "SwitchCase": 1 }],
    'guard-for-in': 'warn',
    'keyword-spacing': 'warn',
    'new-parens': 'warn',
    'no-array-constructor': 'warn',
    'no-caller': 'warn',
    'no-cond-assign': ['warn', 'always'],
    'no-const-assign': 'warn',
    'no-control-regex': 'warn',
    'no-delete-var': 'warn',
    'no-dupe-args': 'warn',
    'no-dupe-class-members': 'warn',
    'no-dupe-keys': 'warn',
    'no-duplicate-case': 'warn',
    'no-empty-character-class': 'warn',
    'no-empty-pattern': 'warn',
    'no-eval': 'warn',
    'no-ex-assign': 'warn',
    'no-extend-native': 'warn',
    'no-extra-bind': 'warn',
    'no-extra-label': 'warn',
    'no-fallthrough': 'warn',
    'no-func-assign': 'warn',
    'no-global-assign': 'warn',
    'no-implied-eval': 'warn',
    'no-invalid-regexp': 'warn',
    'no-iterator': 'warn',
    'no-label-var': 'warn',
    'no-labels': ['warn', { allowLoop: false, allowSwitch: false }],
    'no-lone-blocks': 'warn',
    'no-loop-func': 'warn',
    'no-mixed-operators': ['warn', {
      groups: [
        ['&', '|', '^', '~', '<<', '>>', '>>>'],
        ['==', '!=', '===', '!==', '>', '>=', '<', '<='],
        ['&&', '||'],
        ['in', 'instanceof']
      ],
      allowSamePrecedence: false
    }],
    'no-multi-str': 'warn',
    'no-new-func': 'warn',
    'no-new-object': 'warn',
    'no-new-symbol': 'warn',
    'no-new-wrappers': 'warn',
    'no-obj-calls': 'warn',
    'no-octal': 'warn',
    'no-octal-escape': 'warn',
    'no-redeclare': 'warn',
    'no-regex-spaces': 'warn',
    'no-restricted-syntax': [
      'warn',
      'LabeledStatement',
      'WithStatement',
    ],
    'no-script-url': 'warn',
    'no-self-assign': 'warn',
    'no-self-compare': 'warn',
    'no-sequences': 'warn',
    'no-shadow-restricted-names': 'warn',
    'no-sparse-arrays': 'warn',
    'no-template-curly-in-string': 'warn',
    'no-this-before-super': 'warn',
    'no-throw-literal': 'warn',
    'no-undef': 'warn',
    'no-unexpected-multiline': 'warn',
    'no-unreachable': 'warn',
    'no-unsafe-negation': 'warn',
    'no-unused-expressions': 'warn',
    'no-unused-labels': 'warn',
    'no-unused-vars': ['warn', { vars: 'local', args: 'none' }],
    'no-use-before-define': ['warn', 'nofunc'],
    'no-useless-computed-key': 'warn',
    'no-useless-concat': 'warn',
    'no-useless-constructor': 'warn',
    'no-useless-escape': 'warn',
    'no-useless-rename': ['warn', {
      ignoreDestructuring: false,
      ignoreImport: false,
      ignoreExport: false,
    }],
    'no-with': 'warn',
    'no-whitespace-before-property': 'warn',
    'object-curly-spacing': ['warn', 'always'],
    'operator-assignment': ['warn', 'always'],
    radix: 'warn',
    'require-yield': 'warn',
    'rest-spread-spacing': ['warn', 'never'],
    'semi': 'warn',
    strict: ['warn', 'never'],
    'unicode-bom': ['warn', 'never'],
    'use-isnan': 'warn',
    'valid-typeof': 'warn',

'react/jsx-boolean-value': 'warn',
    'react/jsx-closing-bracket-location': 'warn',
    'react/jsx-curly-spacing': 'warn',
    'react/jsx-equals-spacing': ['warn', 'never'],
    'react/jsx-first-prop-new-line': ['warn', 'multiline'],
    'react/jsx-handler-names': 'warn',
    'react/jsx-indent': ['warn', 2],
    'react/jsx-indent-props': ['warn', 2],
    'react/jsx-key': 'warn',
    'react/jsx-max-props-per-line': 'warn',
    'react/jsx-no-bind': ['warn', {'allowArrowFunctions': true}],
    'react/jsx-no-comment-textnodes': 'warn',
    'react/jsx-no-duplicate-props': ['warn', { ignoreCase: true }],
    'react/jsx-no-undef': 'warn',
    'react/jsx-pascal-case': ['warn', {
      allowAllCaps: true,
      ignore: [],
    }],
    'react/jsx-sort-props': 'warn',
    'react/jsx-tag-spacing': 'warn',
    'react/jsx-uses-react': 'warn',
    'react/jsx-uses-vars': 'warn',
    'react/jsx-wrap-multilines': 'warn',
    'react/no-deprecated': 'warn',
    'react/no-did-mount-set-state': 'warn',
    'react/no-did-update-set-state': 'warn',
    'react/no-direct-mutation-state': 'warn',
    'react/no-is-mounted': 'warn',
    'react/no-unused-prop-types': 'warn',
    'react/prefer-es6-class': 'warn',
    'react/prefer-stateless-function': 'warn',
    'react/prop-types': 'warn',
    'react/react-in-jsx-scope': 'warn',
    'react/require-render-return': 'warn',
    'react/self-closing-comp': 'warn',
    'react/sort-comp': 'warn',
    'react/sort-prop-types': 'warn',
    'react/style-prop-object': 'warn',
    'react/void-dom-elements-no-children': 'warn',

// https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules
    'jsx-a11y/aria-role': 'warn',
    'jsx-a11y/img-has-alt': 'warn',
    'jsx-a11y/img-redundant-alt': 'warn',
    'jsx-a11y/no-access-key': 'warn'
  }
}

.gitignore

Note that we’re ignoring the build directory. This is where Babel will put all of our transpiled components, and we don’t want to push up (or pull down) double the code if we don’t have to.

.DS_Store
build
node_modules
*.log

.npmignore

Note that we’re ignoring the lib directory. Our users will only interact with the transpiled code in build, so we don’t need (or want) to bulk up their node_modules directory with unnecessary code.

.babelrc
lib
CODE_OF_CONDUCT.md

CODE_OF_CONDUCT.md

You might already have a favorite COC. If so, feel free to use it. If not, the Contributor Covenant Code of Conduct is a great one to use.

README.md

Feel free to add whatever you’d like in your README. Some suggested content would be:

  • Title of the project
  • Brief description
  • How to get the project running locally
  • Running the linter
  • Running the test suite
  • How to contribute
  • Steps to submit a PR
  • How to raise issues
  • A link to the Code of Conduct
  • A changelog

Awesome. Now that all of that is settled, we can add some scripts to our package.json

Adding Initial npm Scripts

Inside of our package.json we’ll add these commands to the scripts section:

"scripts": {
  "build": "babel lib -d build",
  "lint": "eslint lib/**; exit 0",
  "lint:watch": "esw -w lib/**",
  "prepublish": "npm run build"
},
  • build will tell Babel to transpile everything in lib and put it in the build directory. Remember how we made the entry build/index.js earlier? That will be the transpiled version of lib/index.js
  • lint will run our linter recursively over lib to make sure our syntax is correct.
  • lint:watch is a nice script that will update the linter anytime a change has been made in lib. It’ll help us catch mistakes as we go.
  • prepublish is a really cool script. npm looks for this when we run npm publish and executes it just before. This is a nice way to make sure our assets in build are the latest versions. We’ll also be adding a lint and test script here later. This helps us ensure we don’t publish broken code to npm.

Cool. Now that we’re done with the basic setup, we can add our first component!

Adding our First Component

Ok. You’ve made it this far. Now it’s time to do something a little more fun. Let’s create a file called Button.js in lib/elements.

$ touch lib/elements/Button.js

Cool. Inside that file, we’ll add this:

import styled from 'styled-components';

const Button = styled.button`
  background: #1FB6FF;
  border: none;
  border-radius: 2px;
  color: #FFFFFF;
  cursor: pointer;
  display: inline-block;
  font-size: 16px;
  line-height: 40px;
  font-weight: 200;
  margin: 8px 0;
  outline: none;
  padding: 0 12px;
  text-transform: uppercase;
  transition: all 300ms ease;
  &:hover {
    background: #009EEB;
  }
`;

export default Button;

Now that we have this setup, we need to add it to lib/index.js so our module knows how to find it.

import Button from './elements/Button’;

module.exports = {
  Button,
};

Wrapping Up

Yay! Our little button component is ready to be published. But before we do that, it would be really nice to have a local test environment to experiment with before we publish to npm. And in Part 2, that’s exactly what we’ll discuss! We’ll also talk about component design and dive into styled-components to make this more customizable.

NOTE: This would be a great place to save your work and commit.

$ git status
$ git add -A
$ git commit -m 'Initial commit | adds basic setup'

I hope this was helpful. If you enjoyed reading, let me know! And if you think it would end helpful for others, feel free to share!

Ready for more? Head to Part 2!


Published by HackerNoon on 2017/06/03