React Project - Idea to Production - Part Two - Setting up a Component Library
Tags: react, typescript, storybook, design
Published: 2020-06-18 02:21:01

This was originally posted [here](https://debojitroy.com/blogs/react-idea-to-production-part-two/)

This is the second post in the series. You can find the first post [here](https://dev.to/debojitroy/react-project-idea-to-production-part-one-wireframes-and-project-setup-b08)

## Where are we

Ok so till now we have

- Brainstormed on our brilliant idea to build a Movie App.
- We have decided what features are needed as part of the MVP.
- Our design team has given us the wireframes.
- We have setup our project as a Monorepo.
- We have setup linting rules, code formatter and commit hooks.

## What are we going to do now

Ok so the next step is to break down our wireframe into components. We will build a component library which can be used across various projects. Finally we will setup storybook to showcase our component library.

## TL;DR

This is a 5 part post

- [Part One : Wireframes and Project Setup](https://debojitroy.com/blogs/react-idea-to-production-part-one)

- Part Two : Setting up a Component Library

- [Part Three : Building the Movie App using component library](https://debojitroy.com/blogs/react-idea-to-production-part-three/)

- [Part Four: Hosting the Movie app and setting up CI/CD](https://debojitroy.com/blogs/react-idea-to-production-part-four/)

Source Code is available [here](https://github.com/debojitroy/movie-app)

Component Library Demo is available [here](https://d3f0roag7dlk8c.cloudfront.net/)

Movie App Demo is available [here](https://d1c9s36vd9mohd.cloudfront.net/)

## Setting the Component Library

Now let's move ahead by setting up our component library.

Move to the `packages` folder

```shell
cd packages
```

Create a new folder for our `components`

```shell
mkdir components
cd components
```

Initialize the yarn project

```shell
yarn init
```

Naming is important here as we will be referring our projects in our workspace using the name. I prefer organization scoped name to avoid naming conflicts. So for our example I will be using `@awesome-movie-app` as our organization name. Feel free to replace with your organization scope.

Next thing to keep in mind is how you want to publish your packages to `npm`. If you would like to publish packages to npm, then make sure the version is semantic and let `lerna` handle the publishing to packages.

If you have a restricted / private NPM organization, make sure to add `publishConfig` with `restricted` access in your `package.json` to avoid accidental publishing of the packages to public npm.

```json
"publishConfig": {
    "access": "restricted"
}
```

As for the purpose of this post, we will not be publishing our packages to npm, so we will skip defining the `publishConfig`.

So our `package.json` looks like

```json
{
  "name": "@awesome-movie-app/components",
  "version": "1.0.0",
  "description": "Component Library for Awesome Movie App",
  "main": "index.js",
  "repository": "git@github.com:debojitroy/movie-app.git",
  "author": "Debojit Roy <debojity2k@gmail.com>",
  "license": "MIT",
  "private": true
}
```

## Defining the requirements

Our project is now setup, lets define our requirements before we move further.

- Our components will be `React` components
- We will use `TypeScript` to build our components
- We want to showcase our components using `Storybook`
- We will use `Bootstrap` for base styles
- We will adopt **CSS-in-JS** and use `StyledComponents`
- We will transpile our code using `Babel`

### Why no Webpack

In a ideal world we will be publishing our packages to `npm`. Before publishing our packages to `npm` we would want to transpile and package them nicely. For this my ideal choice will be webpack.

But one very important feature for libraries is the package should support [Tree Shaking](https://webpack.js.org/guides/tree-shaking/). **Tree Shaking** is a fancy word for trimming excess fat i.e. eliminating code that is not used in the importing library. Due to this known webpack [issue](https://github.com/webpack/webpack/issues/6386), sadly it makes it impossible right now.

To work around the problem we can use [Rollup](https://rollupjs.org/guide/en/), but as we are not interested right now in publishing our package to `npm`, we will use `babel` to transpile our components. I will cover how to use Rollup and tree shake your library in another post.

## Preparing the project

Ok that was way too much theory, now let's move on to setup our project.

Last bit of theory before we move ahead. As we are using `lerna` as our high-level dependency manager, we will use `lerna` to manage dependencies. Which means to add a new dependency we will use this format

```shell
lerna add <dependency-name> --scope=<sub-project-name> <--dev>
```

**dependency-name**: Name of the `npm` package we want to install
**sub-project-name**: This is optional. If you omit this then the dependency will be installed across all the projects. If you want the dependency to be installed only for a specifc project, then pass in the name of the project from individual `package.json`
**--dev**: Same as yarn options. If you want to install only dev dependencies, pass in this flag.

## Adding Project Dependencies

Usually I will go ahead and add most of the dependencies in one command. But for this post I will go verbose explaining each of the dependencies I am adding and the reasoning behind it.

**Note:** We will be adding everything from the **root folder** of the project i.e. the root folder of `movie-app` (one level above `packages` folder)

### Adding React

```shell
lerna add react --scope=@awesome-movie-app/components --dev
lerna add react-dom --scope=@awesome-movie-app/components --dev
```

#### Why one dependency at a time

Sadly due to this [limitation](https://github.com/lerna/lerna/issues/2004) of lerna 😞

#### Why is React a dev dependency 🤔

This part is important. As this library will be consumed in other project, we don't want to dictate our version of `React`, rather we want the consuming project to inject the dependency. So we are going to add common libraries as `dev` dependencies and mark them as peer dependencies. This is true for any common libraries you may want to build.

We will be adding `React` in our peer dependencies of `@awesome-movie-app/components`

```json
"peerDependencies": {
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  }
```

### Adding TypeScript

```shell
lerna add typescript --scope=@awesome-movie-app/components --dev
```

Adding types for `React`

```shell
lerna add @types/node --scope=@awesome-movie-app/components
lerna add @types/react --scope=@awesome-movie-app/components
lerna add @types/react-dom --scope=@awesome-movie-app/components
```

Adding `tsconfig` for TypeScript

```json
{
  "compilerOptions": {
    "outDir": "lib",
    "module": "commonjs",
    "target": "es5",
    "lib": ["es5", "es6", "es7", "es2017", "dom"],
    "sourceMap": true,
    "allowJs": false,
    "jsx": "react",
    "moduleResolution": "node",
    "rootDirs": ["src"],
    "baseUrl": "src",
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "suppressImplicitAnyIndexErrors": true,
    "noUnusedLocals": true,
    "declaration": true,
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "build", "scripts"]
}
```

### Adding Storybook

```shell
lerna add @storybook/react --scope=@awesome-movie-app/components --dev
```

Adding some cool add-ons

```shell
lerna add @storybook/addon-a11y --scope=@awesome-movie-app/components --dev
lerna add @storybook/addon-actions --scope=@awesome-movie-app/components --dev
lerna add @storybook/addon-docs --scope=@awesome-movie-app/components --dev
lerna add @storybook/addon-knobs --scope=@awesome-movie-app/components --dev
lerna add @storybook/addon-viewport --scope=@awesome-movie-app/components --dev
lerna add storybook-addon-styled-component-theme --scope=@awesome-movie-app/components --dev
lerna add @storybook/addon-jest --scope=@awesome-movie-app/components --dev
```

### Adding Test Libraries

We will be using `jest` for unit testing

```shell
lerna add jest --scope=@awesome-movie-app/components --dev
lerna add ts-jest --scope=@awesome-movie-app/components --dev
```

We will be using [enzyme](https://github.com/enzymejs/enzyme) for testing our React Components

```shell
lerna add enzyme --scope=@awesome-movie-app/components --dev
lerna add enzyme-adapter-react-16 --scope=@awesome-movie-app/components --dev
lerna add enzyme-to-json --scope=@awesome-movie-app/components --dev
```

Adding [jest-styled-components](https://github.com/styled-components/jest-styled-components) for supercharing `jest`

```shell
lerna add jest-styled-components --scope=@awesome-movie-app/components --dev
```

Configure `enzyme` and `jest-styled-components` to work with `jest`. We will add `setupTests.js`

```javascript
require("jest-styled-components")
const configure = require("enzyme").configure
const EnzymeAdapter = require("enzyme-adapter-react-16")

const noop = () => {}
Object.defineProperty(window, "scrollTo", { value: noop, writable: true })
configure({ adapter: new EnzymeAdapter() })
```

Configure `jest.config.js`

```javascript
module.exports = {
  preset: "ts-jest",
  // Automatically clear mock calls and instances between every test
  clearMocks: true,

  // Indicates whether the coverage information should be collected while executing the test
  collectCoverage: true,

  // An array of glob patterns indicating a set of files for which coverage information should be collected
  collectCoverageFrom: [
    "src/**/*.{ts,tsx}",
    "!src/**/index.{ts,tsx}",
    "!src/**/styled.{ts,tsx}",
    "!src/**/*.stories.{ts,tsx}",
    "!node_modules/",
    "!.storybook",
    "!dist/",
    "!lib/",
  ],

  // The directory where Jest should output its coverage files
  coverageDirectory: "coverage",

  // An array of regexp pattern strings used to skip test files
  testPathIgnorePatterns: ["/node_modules/", "/lib/", "/dist/"],

  // A list of reporter names that Jest uses when writing coverage reports
  coverageReporters: ["text", "html", "json"],

  // An array of file extensions your modules use
  moduleFileExtensions: ["ts", "tsx", "js", "jsx"],

  // A list of paths to modules that run some code to configure or set up the testing framework before each test
  setupFilesAfterEnv: ["./setupTests.js"],

  // A list of paths to snapshot serializer modules Jest should use for snapshot testing
  snapshotSerializers: ["enzyme-to-json/serializer"],
}
```

### Adding Styled Components and BootStrap

```shell
lerna add styled-components --scope=@awesome-movie-app/components --dev
lerna add react-bootstrap --scope=@awesome-movie-app/components --dev
lerna add bootstrap --scope=@awesome-movie-app/components --dev

lerna add @types/styled-components --scope=@awesome-movie-app/components
```

### Adding Babel

As we will be using babel to transpile everything. Its important we configure Babel properly.

Adding Babel Dependencies

```shell
lerna add @babel/core --scope=@awesome-movie-app/components --dev
lerna add babel-loader --scope=@awesome-movie-app/components --dev
lerna add @babel/cli --scope=@awesome-movie-app/components --dev
lerna add @babel/preset-env --scope=@awesome-movie-app/components --dev
lerna add @babel/preset-react --scope=@awesome-movie-app/components --dev
lerna add @babel/preset-typescript --scope=@awesome-movie-app/components --dev
lerna add core-js --scope=@awesome-movie-app/components --dev
```

A bit on the `babel` components we added

- **@babel/core** : Core `babel` functionality
- **babel-loader** : Used by `storybook` `webpack` builder
- **@babel/cli** : Will be used by us to transpile files from command line
- **@babel/preset-env** : Environment setting for transpiling
- **@babel/preset-react** : React setting for `babel`
- **@babel/preset-typescript** : TypeScript settings for `babel`
- **core-js** : Core JS for `preset-env`

Now let's add our `.babelrc` file

```json
{
  "presets": [
    "@babel/preset-typescript",
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "corejs": "3",
        "modules": false
      }
    ],
    "@babel/preset-react"
  ]
}
```

## Bringing it all together

### Important Note

The steps below may differ based on which version of `Storybook` and `Jest` you are using. The below steps are written for `Storybook` `v5.3+` and `Jest` `v26.0+`

### Setting up our theme

First step will be to setup our `theme`. We can start with a blank `theme` and fill it up as we go.

```shell
cd packages/components
mkdir theme
```

Defining the `Theme`

```typescript
export interface Theme {
  name: string
  color: {
    backgroundColor: string
    primary: string
    secondary: string
  }
}
```

Defining `Light` theme

```typescript
import { Theme } from "./theme"

const lightTheme: Theme = {
  name: "LIGHT",
  color: {
    backgroundColor: "#fff",
    primary: "#007bff",
    secondary: "#6c757d",
  },
}

export default lightTheme
```

Defining `Dark` theme

```typescript
import { Theme } from "./theme"

const darkTheme: Theme = {
  name: "DARK",
  color: {
    backgroundColor: "#000",
    primary: "#fff",
    secondary: "#6c757d",
  },
}

export default darkTheme
```

### Setting up Storybook

To configure `storybook`, we need to setup the configuration folder first. We will use the default `.storybook` folder, but feel free to use folder name.

```shell
mkdir .storybook
```

Now inside `.storybook` folder we will create the configuration files needed for `storybook`

#### main.js

This is the `main` configuration file for storybook. We will configure the path for `stories`, register our `addons` and override `webpack` config to process `typescript files`.

```javascript
// .storybook/main.js

module.exports = {
  stories: ["../src/**/*.stories.[tj]sx"],
  webpackFinal: async config => {
    config.module.rules.push({
      test: /\.(ts|tsx)$/,
      use: [
        {
          loader: require.resolve("ts-loader"),
        },
      ],
    })
    config.resolve.extensions.push(".ts", ".tsx")
    return config
  },
  addons: [
    "@storybook/addon-docs",
    "@storybook/addon-actions/register",
    "@storybook/addon-viewport/register",
    "@storybook/addon-a11y/register",
    "@storybook/addon-knobs/register",
    "storybook-addon-styled-component-theme/dist/register",
    "@storybook/addon-jest/register",
  ],
}
```

#### manager.js

Here we configure the Storybook manager. There are many options that can be overriden, for our project we want the add-ons panel to be at the `bottom` (default is `right`)

```javascript
// .storybook/manager.js

import { addons } from "@storybook/addons"

addons.setConfig({
  panelPosition: "bottom",
})
```

#### preview.js

Finally we will configure the Story area. We intialize our add-ons and pass global configurations.

```javascript
// .storybook/preview.js
import { addParameters, addDecorator } from "@storybook/react"
import { withKnobs } from "@storybook/addon-knobs"
import { withA11y } from "@storybook/addon-a11y"
import { withThemesProvider } from "storybook-addon-styled-component-theme"
import { withTests } from "@storybook/addon-jest"
import results from "../.jest-test-results.json"
import lightTheme from "../theme/light"
import darkTheme from "../theme/dark"

export const getAllThemes = () => {
  return [lightTheme, darkTheme]
}

addDecorator(withThemesProvider(getAllThemes()))

addDecorator(withA11y)
addDecorator(withKnobs)

addDecorator(
  withTests({
    results,
  })
)

addParameters({
  options: {
    brandTitle: "Awesome Movie App",
    brandUrl: "https://github.com/debojitroy/movie-app",
    showRoots: true,
  },
})
```

### Creating React Components

Now we can create our very first react component.

#### Our first button

We will first create a `src` folder

```shell
mkdir src && cd src
```

Then we will create a folder for our component. Let's call it `Sample`

```shell
mkdir Sample && cd Sample
```

Now let's create a simple `styled` `button` and pass some props to it.

```typescript
// styled.ts
import styled from "styled-components"

export const SampleButton = styled.button`
  background-color: ${props => props.theme.color.backgroundColor};
  color: ${props => props.theme.color.primary};
`
```

```typescript
// Button.tsx
import React from "react"
import { SampleButton } from "./styled"

const Button: React.FC<{
  value: string
  onClickHandler: () => void
}> = ({ value, onClickHandler }) => (
  <SampleButton onClick={onClickHandler}>{value}</SampleButton>
)

export default Button
```

Awesome !!! We have our first component finally !!!

#### Adding unit tests

Now let's add some tests for our new button.

```shell
mkdir tests
```

```typescript
// tests/Button.test.tsx

import React from "react"
import { mount } from "enzyme"
import { ThemeProvider } from "styled-components"
import lightTheme from "../../../theme/light"
import Button from "../Button"

const clickFn = jest.fn()
describe("Button", () => {
  it("should simulate click", () => {
    const component = mount(
      <ThemeProvider theme={lightTheme}>
        <Button onClickHandler={clickFn} value="Hello" />
      </ThemeProvider>
    )
    component.find(Button).simulate("click")
    expect(clickFn).toHaveBeenCalled()
  })
})
```

#### Adding stories

Now with the new button in place, lets add some `stories`

```shell
mkdir stories
```

We will use the new [Component Story Format (CSF)](https://storybook.js.org/docs/formats/component-story-format/)

```typescript
// stories/Button.stories.tsx

import React from "react"
import { action } from "@storybook/addon-actions"
import { text } from "@storybook/addon-knobs"
import Button from "../Button"

export default {
  title: "Sample / Button",
  component: Button,
}

export const withText = () => (
  <Button
    value={text("value", "Click Me")}
    onClickHandler={action("button-click")}
  />
)

withText.story = {
  parameters: {
    jest: ["Button.test.tsx"],
  },
}
```

### Time to check if everything works

#### Transpiling our code

As we discussed in the beginning, we will be using `babel` to transpile our code and let the calling projects take care of minification and tree-shaking.

So going ahead with that, we will add some scripts and test they are working.

##### Typecheck and Compilation

We will first use `TypeScript`'s compile to compile our code.

```json
"js:build": "cross-env NODE_ENV=production tsc -p tsconfig.json"
```

If everything is fine, we should see an output like this

```shell
$ cross-env NODE_ENV=production tsc -p tsconfig.json
✨  Done in 5.75s.
```

##### Transpiling with Babel

Next step will be to transpile our code with `babel`

```json
"build-js:prod": "rimraf ./lib && yarn js:build && cross-env NODE_ENV=production babel src --out-dir lib --copy-files --source-maps --extensions \".ts,.tsx,.js,.jsx,.mjs\""
```

If everything is fine, we should see an output like this

```shell
$ rimraf ./lib && yarn js:build && cross-env NODE_ENV=production babel src --out-dir lib --copy-files --source-maps --extensions ".ts,.tsx,.js,.jsx,.mjs"
$ cross-env NODE_ENV=production tsc -p tsconfig.json
Successfully compiled 4 files with Babel.
✨  Done in 7.02s.
```

##### Setting up watch mode for development

During development, we would like incremental compilation every time we make changes. So let's add a watch script.

```json
"js:watch": "rimraf ./lib && cross-env NODE_ENV=development concurrently -k -n \"typescript,babel\" -c \"blue.bold,yellow.bold\"  \"tsc -p tsconfig.json --watch\" \"babel src --out-dir lib --source-maps --extensions \".ts,.tsx,.js,.jsx,.mjs\" --copy-files --watch --verbose\""
```

We should see output like this

```shell
Starting compilation in watch mode...
[typescript]
[babel] src/Sample/Button.tsx -> lib/Sample/Button.js
[babel] src/Sample/stories/Button.stories.tsx -> lib/Sample/stories/Button.stories.js
[babel] src/Sample/styled.ts -> lib/Sample/styled.js
[babel] src/Sample/tests/Button.test.tsx -> lib/Sample/tests/Button.test.js
[babel] Successfully compiled 4 files with Babel.
[typescript]
[typescript] - Found 0 errors. Watching for file changes.
```

#### Running Unit Tests

Once we are sure our compilation and transpiling works, lets make sure our tests work.

```json
"test": "jest"
```

Running our tests should show an output similar to this

![Jest Test Suite Output](https://dev-to-uploads.s3.amazonaws.com/i/6c0xrucs12j1rhw5eeev.png)

We are getting there slowly 😊

Now we need to generate `json` output for storybook to consume and show next to our stories. Let's configure that as well.

```json
"test:generate-output": "jest --json --outputFile=.jest-test-results.json || true"
```

#### Running Storybook

Finally we want to run storybook with our stories. Lets run storybook in dev mode.

```json
"storybook": "start-storybook -p 8080"
```

If everything was configured properly, we should see the storybook in our [Browser](http://localhost:8080)

![Local Storybook in Browser](https://dev-to-uploads.s3.amazonaws.com/i/ay7iasah5nht14w4h2fo.png)

We will add couple of more commands for building storybook for deployment. We will be using these when we configure Continuous Deployment in our last post - [Part Four: Hosting the Movie app and setting up CI/CD](https://debojitroy.com/blogs/react-idea-to-production-part-four/)

```json
"prebuild:storybook": "rimraf .jest-test-results.json && yarn test:generate-output",
"build:storybook": "build-storybook -c .storybook -o dist/"
```

After this we can start splitting our wireframes into components. I will not go into the details of that as there are much better posts out there which do a better job of explaining the process. You can find the code that we complete till now [here](https://github.com/debojitroy/movie-app/tree/master/packages/components)

In the next part we will setup and build our movie app, continue to [Part Three : Building the Movie App using component library](https://debojitroy.com/blogs/react-idea-to-production-part-three/)