Building component library with Docz and Lerna

webdev lerna docz react component library

Image credit: Tim Johnson on Unsplash

Component libraries are all the rage these days, with many companies rolling out their own solutions or sticking to a bunch of open source alternatives. Leveraging a component library for UI development, particularly in large teams, has a lot of cool benefits. It allows to take full advantage of modular and reusable UI components, which brings increased speed of development and unifies styles across multiple teams and apps. Combine that with a robust design system, and the handover from design to development teams becomes smooth and more efficient. 

Frameworks/libraries like React, Vue, etc are perfectly suited for this purpose since there are designed to be highly modular. In this post React and Styled components are used as main tools of choice for developing components.

There are also some helpful tools, that could be leveraged to speed up the development process and deployment of the library. Embracing the modular approach, it would make sense that each component would be an own npm package, the whole library being a monorepo. That's where Lerna will be used to manage multiple packages inside the project, as well as to keep track of their versioning and publishing process. 

In order to test and document the components, Docz is used (as an alternative to Storybook). It allows documenting components with MDX, which is a format that combines JSX and Markdown, basically making it possible to import React components inside Markdown files. Moreover, Docz version 2 runs on GatsbyJS, which brings increased development and build speeds and enables access to Gatsby's vast network of plugins and tools.

Lerna setup

We'll start by creating a new project, titled uikit, and installing the required dependencies.

$ npm i -g lerna
$ mkdir uikit && cd $_
$ yarn add docz react react-dom styled-components

With the core dependencies installed, it's time to initialize the Lerna project.

$ lerna init

This is will create the following project structure: 

ui-kit/
  packages/
  package.json
  lerna.json

The UI components will be stored in the packages folder.

Now let's examine the generated lerna.json, which serves as a configuration file for Lerna. By default there isn't much going on, and after a few customizations the config will look as follows.

{
  "npmClient": "yarn",
  "version": "independent",
  "packages": [
    "packages/*"
  ],
  "useWorkspaces": true
}

The most important changes here are selecting yarn as npm client, specifying independent versioning, so the package versions can be changed independently of each other, and enabling Yarn workspaces.  The packages option points to the location of our library packages, for which we'll keep the default setting. The more extensive list of configuration options is available on Lerna's Github page.

Additionally, we'll need to add workspaces-related options to the root package.json.

{
  "name": "uikit",
  "license": "MIT",
  "workspaces": {
    "packages": [
      "packages/*"
    ]
  },
  "private": true,
  "dependencies": {
    "docz": "^2.2.0",
    "lerna": "^3.20.2",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "styled-components": "^5.0.0"
  },
  "devDependencies": {
    "prettier": "^1.19.1"
  }
}

Here we specify the path to workspaces, which is the same as the one in lerna.json. Also we have to make the package private, otherwise workspaces won't work.

Creating the first component

To kick things off with the dev work, let's add the first package - Typography, with the necessary base font components. As a result, the project's structure will be updated as follows.

ui-kit/
  packages/
    typography/
      src/
        index.js
      CHANGELOG.md
      package.json
  package.json
  lerna.json

Before actually writing the font components, let's make a few modifications to the typography's package.json.

{
  "name": "@uikit/typography",
  "version": "1.0.0",
  "description": "Base fonts",
  "main": "dist/index.js",
  "module": "src/index.js",
  "files": [
    "dist",
    "CHANGELOG.md"
  ],
  "author": "",
  "license": "MIT"
}

The most interesting here are main, module and files fields. We'll point main to the dist folder, where the transpiled files will be stored and later used in the installed package. The module will point to the src folder, so the packages can be imported directly from the source folder during development and the changes will be reflected immediately without needing to bootstrap packages again or run build script. Finally the files property contains the list of the files, which will be included in the published package.

Now we can setup some basic font styles in typography's index.js. Those will be made as styled components.

// typography/src/index.js

import styled, { css } from "styled-components";

const fontFamily = "sans-serif";
const fontWeights = {
  light: 300,
  regular: 400,
  bold: 600
};

const baseStyles = css`
  font-family ${fontFamily};
  margin: 0;
  padding: 0; 
  -webkit-font-smoothing: antialiased;
  font-weight: ${({ fontWeight }) => fontWeights[fontWeight] || fontWeights.regular};
`;

export const H1 = styled.h1`
  ${baseStyles};
  font-size: 62px;
  letter-spacing: -3px;
  line-height: 62px;
`;

export const H2 = styled.h2`
  ${baseStyles};
  font-size: 46px;
  letter-spacing: -3px;
  line-height: 46px;
`;

export const H3 = styled.h3`
  ${baseStyles};
  font-size: 30px;
  letter-spacing: -2px;
  line-height: 30px;
`;

export const H4 = styled.h4`
  ${baseStyles};
  font-size: 24px;
  letter-spacing: -1.5px;
  line-height: 24px;
`;

export const H5 = styled.h5`
  ${baseStyles};
  font-size: 20px;
  letter-spacing: -1px;
  line-height: 20px;
`;

export const H6 = styled.h6`
  ${baseStyles};
  font-size: 18px;
  letter-spacing: 0;
  line-height: 18px;
`;

export const Text = styled.p`
  ${baseStyles};
  font-size: 16px;
  letter-spacing: 0;
  line-height: 16px;
`;

export const SmallText = styled.small`
  ${baseStyles};
  font-size: 12px;
  letter-spacing: 0;
  line-height: 12px;
`;

Note that css helper from styled-components is used to define reusable parts of the styles, which are then extended by other components. The components also accept a fontWeight property for customization, which defaults to regular

Trying out Docz's playground

This seems like a good time to try these components out in action and that's where Docz will be used to document their usage. In order to do that, we'll need to add an .mdx file somewhere in the project with the component documentation, and one of those files needs to point to route: / and will be used as the front page. Let's create this index.mdx in the root of the packages.

// index.mdx

---
name: Welcome
route: /
---

# Welcome to the awesome UI Kit

Select any of the components from the sidenav to get started. 

After running yarn docz dev, we can navigate to localhost:3000 and see the front page of the library.

 

To add documentation to the typography, we'll create a docs folder inside the package and add typography.mdx there.

ui-kit/
  packages/
    typography/
      docs/
        typography.mdx 
      src/
        index.js
      CHANGELOG.md
      package.json
  package.json
  lerna.json

To document components, we'll use a special docz component, called Playground. Wrapping it around the components will allow editing them right below where they are displayed.

---
name: Typography
menu: Components
---

import { Playground } from 'docz';
import { H1, H2, H3, H4, H5, H6, Text, SmallText } from '../src/index';

# Base Typography
<Playground>
    <H1>Heading 1</H1>
    <H2>Heading 2</H2>
    <H3>Heading 3</H3>
    <H4>Heading 4</H4>
    <H4 fontWeight='bold'>Heading 4 bold</H4>
    <H5>Heading 5</H5>
    <H6>Heading 6</H6>
    <Text>Text</Text>
    <SmallText>SmallText</SmallText>
</Playground>

After refreshing the page, or restarting dev sever if necessary, we'd be able to see our typography components. And the best thing is that we can directly edit the code on the page and see the updated results immediately! 

Adding custom fonts

This works well for built-in fonts, but what if we want to load a custom font, say from Google fonts? Unfortunately, since v2 of Docz has been released quite recently and due to it being a major rewrite of v1, there's still no clear, documented way to do that. However, there's one solution, which also nicely demonstrates the extendability of Gatsby configuration and a concept, known as Component shadowing.

For Gatsby-specific components we'll need to create a src folder in the root of the project, where the theme-specific components, among others, will be stored. Since we're extending gatsby-theme-docz, a folder with this name needs to be created inside the src. Lastly, we'll create a wrapper.js file inside of it to have the following project structure.

ui-kit/
  packages/
    typography/
      docs/
        typography.mdx
      src/
        index.js
      CHANGELOG.md
      package.json
  src/
    gatsby-theme-docz/
      wrapper.js
  package.json
  lerna.json

Inside wrapper.js we'll add a very simple component, the only task of which is to pass down its children.

// src/gatsby-theme-docz/wrapper.js

import React, { Fragment } from "react";

export default ({ children }) => <Fragment>{children}</Fragment>;

It seems quite pointless to make a component which only forwards the children, however the reason for this is that we can now include css styles in this component, which will be applied globally. For that, let's create styles.css alongside wrapper.js and import there one of the selected fonts. In this tutorial, we'll be using Montserrat. 

/* src/gatsby-theme-docz/styles.css */

@import url('https://fonts.googleapis.com/css?family=Montserrat:300,400,600&display=swap');

Now we just need to import this file into wrapper.js and update the fontFamily constant for the typography.

// src/gatsby-theme-docz/wrapper.js

import React, { Fragment } from "react";
import "./style.css";

export default ({ children }) => <Fragment>{children}</Fragment>;
// ./packages/typography/src/index.js

import styled, { css } from "styled-components";

const fontFamily = "'Montserrat', sans-serif";

// ...

The changes should be visible immediately (if not, might need to restart the dev server). This might not be the cleanest approach, but it gets the job done, and since it's no longer possible to load custom fonts via doczrc.js, this might be one of the few viable solutions. 

Customizing the documentation site

Talking about doczrc.js, which is used to configure a Docz project. The list of configuration options can be found on the project's documentation site. Since we're now using Montserrat font for UI kit's typography, it would make sense if our documentation website used the same font. To do that, we'll add a themeConfig property to the doczrc.js, where the styles for the most commonly used text elements will be applied.

const fontFamily = "'Montserrat', sans-serif";

export default {
  title: "UI Kit",
  description: "UI Kit - Collection of UI components",
  themeConfig: {
    styles: {
      h1: {
        fontFamily: fontFamily
      },
      h2: {
        fontFamily: fontFamily
      },
      body: {
        fontFamily: fontFamily
      }
    }
  }
};

Since we need to keep our project configuration separate from the components, we'll have to declare the font family separately here and use it for specific text elements. Additionally, we can customize the project title and description here. The default themeConfig can be found on the Docz's Github page. More options to customize the project, like adding a custom logo, are described in the documentation.

Adding Buttons

Finally it's time to add a React component, Buttons, which will also make use of the typography for better illustration of how components can be used together. As before, we'll make a new package, so the project's structure will be as follows.

ui-kit/
  packages/
    typography/
      docs/
        typography.mdx
      src/
        index.js
      CHANGELOG.md
      package.json
    buttons/
      docs/
        buttons.mdx
      src/
        index.js
        Buttons.js
      CHANGELOG.md
      package.json
  src/
    gatsby-theme-docz/
      style.css
      wrapper.js
  package.json
  lerna.json

The package.json for buttons will look almost identical to the one from typography, with a few small exceptions. The most notable one is that buttons has typography package as a dependency.

{
  "name": "@uikit/buttons",
  "version": "1.0.0",
  "description": "Button components",
  "main": "dist/index.js",
  "module": "src/index.js",
  "files": [
    "dist",
    "CHANGELOG.md"
  ],
  "dependencies": {
    "@uikit/typography": "^1.0.0"
  },
  "author": "",
  "license": "MIT"
}

Now, after we run lerna bootstrap, it will install all the required packages and symlink the dependencies inside the packages folder. One nice benefit of this is that if we make any changes to the typography package and use that package inside buttons, the changes will be immediately reflected in both packages without needing to rebuild or publish any of them. This makes the development experience really fast and efficient! 

After all the dependencies have been installed, we can start writing code for the buttons.

// packages/buttons/src/Buttons.js

import React from "react";
import styled from "styled-components";

import { SmallText } from "@uikit/typography";

export const ButtonSmall = ({ text, ...props }) => {
  return (
    <Button {...props}>
      <SmallText>{text}</SmallText>
    </Button>
  );
};

export const Button = styled.button`
  border-radius: 4px;
  padding: 8px 16px;
  color: white;
  background-color: dodgerblue;
  border-color: dodgerblue;
`;
// packages/src/buttons/index.js

export * from "./Buttons";

Here we define two very basic button components. The Button component has a few base styles, which could be further extended. ButtonSmall has a predefined text component and therefore accepts button text as a separate prop. Additionally we export everything from Buttons.js inside index.js as a convenience. This will ensure a single point of export for each package, particularly helpful when there are multiple files per package. Now let's try these new components out in the playground.

// packages/buttons/docs/buttons.mdx

---
name: Buttons
menu: Components
---

import { Playground } from 'docz';
import { Button, ButtonSmall } from '../src/index';

# Buttons

## Base button
<Playground>
    <Button>Test</Button>
</Playground>



## Small button
<Playground>
    <ButtonSmall text='Click me'/>
</Playground>

Navigating back to localhost:3000 we can confirm that the buttons work as expected. With that we have a properly documented, functioning component library, which can be easily extended. 

Deploying the docs and publishing packages

So far the we have been focusing mostly on development side of the component library, however there are a few other important steps that need to happen before the library becomes usable. 

Publishing packages

To publish all the packages that have been changed since the last publishing took place (and after they have been transpiled with Babel), we can use lerna publish command. It will prompt to specify versioning for each package before publishing them. The version can be specified directly with the publish command, which will apply the same versioning to all the changed packages and will skip the prompts, e.g. lerna publish minor. For publishing to work, a registry needs to be added in lerna.json.

 "command": {
    "publish": {
      "registry": "https://mypackageregistry/"
    }
  }

Building the docs and serving them

Docz comes with a few built-in scripts that make it easier to view and deploy the documentation. It can be built and served locally by running yarn docs build && yarn docz serve. To deploy the documentation online Docz's site has a handy example of doing it with Netlify. After Netlify site has been setup, deploying is easy via running netlify deploy --dir .docz/dist.

If you want to have a look at the boilerplate code for the component library, it's available on my Github.