Boilerplate for publishing components and its storybook

Let’s say we have an idea for a cool component. We want to build it and share it with the community by publishing it to NPM registry. The code to implement the component functionality appears intuitive. But the boilerplate we have to put together for the open-source project is convoluted. In this tutorial, we will put together a boilerplate project for publishing components.

If you don’t like reading text, there is a github repository which you can quickly go over to. This tutorial explains how I put together that github repository.

The objective of the boilerplate is to:

  • Compile TypeScript code to JavaScript
  • Run unit tests
  • Visually test the component in a Storybook
  • Publish the component to NPM and Storybook to Netlify

The component we are putting together is a simple Counter component. To view the published storybook for the component, please click here.

A) Compile source code

We are going to use TypeScript for writing our component. For compiling the TypeScript code to JavaScript code, we will use a package called Rollup. Why do we use Rollup instead of Webpack? Webpack is useful to put together complex React apps. But for building out just a single component, Rollup is good. Most component developers use Rollup for compilation.

Create a new project using yarn init --y.

Add the following dev dependencies. (Our component is simple, So everything in this tutorial is a dev dependency).

yarn add --dev react typescript rollup rollup-plugin-typescript2

Create a rollup.config.js file and paste the following configuration.

import Ts from "rollup-plugin-typescript2";

export default {
  input: ["src/index.ts"],
  output: {
    dir: "lib",
    format: "esm",
    sourcemap: true,
  },
  plugins: [Ts()],
  external: ["react"],
};

The entrypoint for compilation is src/index.ts. We ask rollup to output the compilation results to a lib folder. The emitted JavaScript is in esm format or ES modules. Rollup plugin for TypeScript will eventually use the TypeScript compiler for compilation. And we mark React as an external package. Though we import React in our component files, we are not going to include React in the output bundle.

Create a tsconfig.json file that has the settings for TypeScript compiler.

{
  "compilerOptions": {
    "outDir": "lib",
    "module": "esnext",
    "target": "esnext",
    "lib": ["esnext", "dom"],
    "sourceMap": true,
    "allowJs": true,
    "moduleResolution": "node",
    "jsx": "react",
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUnusedParameters": true,
    "declaration": true,
    "allowSyntheticDefaultImports": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "lib"]
}

For compilation, add a build script to package.json that invokes rollup like so:

  "scripts": {
    "build": "rollup -c",
  }

Now, it is time to put together our awesome Counter component.

B) Write the Counter component

In this section, we will put together the Counter component quickly. Please feel free to skip the section as this is standard React stuff.

The Counter component has a button and a display. When the user clicks on the button, the counter increments and updates the display. The developer can set a default value to the counter (like setting it to 1000) when the component mounts. That is all there is to the Counter component.

Counter component

Create a src folder.

Within the src folder, create a Button.tsx file:

import React from "react";

interface ButtonProps {
  onClick: () => void;
}

const Button: React.FC<ButtonProps> = ({ onClick }) => {
  return <button onClick={onClick}>+1</button>;
};

export default Button;

Create a Count.tsx file:

import React from "react";

interface CountProps {
  count: number;
}

const Count: React.FC<CountProps> = ({ count }) => {
  return <span>{count}</span>;
};

export default Count;

The Counter component (Counter.tsx) has the Button component and the Count component. When the user clicks the Button, the click handler is a callback from the Counter component. We increment a counter stored as state. The Count component displays the newly incremented counter within a span. (It is easier to show the code rather than explain it!). Create a Counter.tsx file:

import React, { useState, useEffect } from "react";
import Button from "./Button";
import Count from "./Count";

export interface CounterProps {
  defaultCount?: number;
}

const Counter: React.FC<CounterProps> = ({ defaultCount = 0 }) => {
  const [count, setCount] = useState<number>(defaultCount);
  useEffect(() => {
    setCount(defaultCount);
  }, [defaultCount]);
  function handleClick() {
    setCount((count) => count + 1);
  }
  return (
    <div>
      <Button onClick={handleClick} />
      <Count count={count} />
    </div>
  );
};

export default Counter;

Please make sure that you export the component along with its props, i.e. export the CounterProps interface as well.

Finally, create an index.ts file:

export { default } from "./Counter";

Run yarn build and verify that compiled files are emitted in the lib folder.

C) Unit testing the Counter

We will unit test the counter component using React testing library and Jest. React testing library has a virtual DOM into which it can render components. There are methods in React testing library to query the DOM and interact with it (fire events). Jest is a test-runner that can run all our functional tests. Jest uses babel for compilation.

Install the following dev dependencies:

yarn add --dev
jest
@types/jest
@testing-library/react
@testing-library/jest-dom
@babel/core
@babel/preset-env
@babel/preset-react
@babel/preset-typescript
react-dom

React testing library has react-dom as peer dependency!

Add a babel.config.js to configure babel:

module.exports = {
  presets: [
    ["@babel/preset-env", { targets: { node: "current" } }],
    "@babel/preset-typescript",
    "@babel/preset-react",
  ],
};

Add a jest.config.js to configure Jest:

module.exports = {
  roots: ["<rootDir>/src"],
  setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
  testEnvironment: "jsdom",
};

Jest looks for tests in the src folder matching a default regex. For example, if we want to write tests for the Counter component, we will create a file Counter.test.tsxand write our tests there. Jest will pick the tests in that file and run it.

There is also some additional setup that React Testing Library needs to do. So, we specify the setupFilesAfterEnv config to do that.

Create a jest.setup.ts file:

import "@testing-library/jest-dom";

We are now all set to write our first (and only!) test. In the src folder, create a Counter.test.tsx file:

import React from "react";
import { render, screen } from "@testing-library/react";
import Counter from "./Counter";

test("displays a button", () => {
  render(<Counter />);
  const button = screen.getByRole("button", { name: /\+1/i });
  expect(button).toBeInTheDocument();
});

In the above test, we test if the Counter component renders a button. To do that, render the component in Virtual DOM using the render function. Get the button from the screen using getByRole. Assert that the button exists in the DOM. Since, we have a Button in the Counter component, the test should pass.

Add a test script to package.json:

"test": "jest --verbose --watch",

Run yarn test and verify that the test passes.

D) Use Storybook for visual testing

Storybook is a great framework for quickly testing the components that we write. The API for Storybook keeps changing quite a bit. At the time of writing this tutorial, we are using v6 of the API.

The recommended way of setting up Storybook in a React project is using npx sb init command. But using the init command installs quite a lot of dependencies (addons in Storybook terminology) as well as creates some default stories (unrelated to the project). To avoid all that, I recommend a manual approach of setting up Storybook.

Add the following dev dependencies:

yarn add --dev
@storybook/react
@storybook/addon-essentials
babel-loader

From the root folder, create a .storybook folder. In that, create a main.js file:

module.exports = {
  stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: ["@storybook/addon-essentials"],
};

The above configuration tells Storybook where to pick the stories from – Look for stories in the src folder with .stories.in the filename.

Create a Counter.stories.tsx file in the src folder:

import React from "react";
import Counter, { CounterProps } from "./Counter";

export default {
  title: "Counter",
  component: Counter,
};

const Template = (args: CounterProps) => <Counter {...args} />;

export const Basic = Template.bind({});
Basic.args = {};
export const WithDefaultCounter = Template.bind({});
WithDefaultCounter.args = { defaultCount: 1000 };

Unfortunately, for Storybook to work correctly, we have to structure code this way. The default export ensures we have a section for our component in Storybook. Then we write all the variations for our component. In our case, there are two variations: one with no props and the other with the defaultCount prop set. Export all those variations. Storybook will pick it up.

Storybook has two binaries or apps: start-storybook and build-storybook. Start storybook command runs the storybook in dev mode. Whereas build storybook command emits static files in a folder storybook-static that we can publish to a hosting provider like Netlify.

Add storybook commands to the script section in package.json:

    "storybook": "start-storybook",
    "build-storybook": "build-storybook"

Run yarn storybook. We should see a browser page like so:

Storybook

We are almost done with the boilerplate. It is time to publish the component as NPM package and the storybook as a static site to Netlify.

E) Publish the component and the storybook

We have to make a few changes to the package.json to get ready for doing the publish.

Change the name to a scoped package. (@vijayt is the scope)

"name": "@vijayt/counter"

Set main in package.json. When any app uses an import for our component, this is the file that the bundler picks up.

"main": "lib/index.js"

Set the files in package.json. All folders mentioned in the files will get uploaded to NPM registry when we publish. In our case, it is the lib folder.

"files": [
  "lib"
]

To publish scoped packages, we have to do a few changes in NPM website. Login and click on your profile. Then click “Add organisation”. Or navigate to https://www.npmjs.com/org/create.

New org

Type the name of your organisation and click “Create” next to “Unlimited public packages”. (Make sure you change the package name from @vijayt/counter to @<your-org-name>/counter in package.json).

We are all set to publish. By default, when we publish a package with a scope, NPM tries to publish it as a private package. To prevent that we publish (only for the first time) with an access flag.

npm login
npm publish --access public

Great! Now, it is time to publish our Storybook.

Running yarn build-storybook will build Storybook. We have to do it from Netlify. I am assuming most of you are familiar with Netlify and how to set it up. So, I am going to show you only the relevant config from Netlify.

Netlify config

Summary

In this tutorial, we walked through the steps to create a boilerplate if you want to write a cool component and publish it to the open source community. The entire source code is available in a Git repo. And the Storybook is available in Netlify.

Related Posts

Leave a Reply

Your email address will not be published.