Creating a React Component Library With Typescript Using Styleguidist
How to build a reusable component library, publish and automate the release flow
One of the beauties of React is component reusability, which is a huge time saver. Most times, creating new components for a project may mean that you may have to do the same for every new project which will ultimately result in unavoidable code duplication.
Having a component library ensures that reusable components can be packaged and shared across multiple applications, meaning that you do not have to worry about code duplication, makes it faster to build new applications and helps speed up scalable application development. Components can easily be customised at the project level for each application they belong to. It also means that issues such as bug fixes and adding new features can be done at source and pulled through on every project that needs it.
In this post, I explain how to create a React component library using the following tools:-
- Create React App with Typescript
- React styleguidist — to help with component development
- Styled-components — allows you to style your components by utilising the power of CSS and tagged template literals.
- Atomic Design — introduced by Brad Frost and helps in designing systems of components
- Testing using Jest with Enzyme
- Automated deployment using GitHub, CircleCI and Netlify
- Semantic release - automates version management and package publishing.
Development
Create a new React project with CRA using npx
for the latest version
// Using npx
$ npx create-react-app react-component-library --template typescript$ cd react-component-library// Start app
$ yarn start
The next step is to install dependencies required by our project:
// Install babel compiler and plugins
$ yarn add @babel/cli @babel/node
$ yarn add -D @babel/core @babel/preset-env @babel/preset-typescript babel-plugin-styled-components // Install styled components and dependencies
$ yarn add styled-components @types/styled-components
$ yarn add -D typescript-plugin-styled-components// Install react styleguidist and dependencies
$ yarn add react-styleguidist// Generates styleguidist documentation from Typescript
$ yarn add -D react-docgen-typescript// Adds documentation to components
$ yarn add prop-types
Configuration
After installing all dependencies, you should create a configuration file for babel babel.config.js
at the root of your project and add the following:
Next, we need to add a configuration file for React-styleguidist, styleguide.config.js
at the root of your project and add the following:
You can find out more about React-Styleguidist configuration.
Documentation
We need to add some documentation for our library. In this case, I create a docs
folder at the project root and create an introduction.md
file
// At project root
$ mkdir docs
$ cd docs
$ touch introduction.md
And added some content about our library:
Theme
The next step is for us to create a theme for our project. You can create as many project specific themes as you want, especially if your have multiple projects that need to use different styles.
For this demo, I will be creating a theme for a project1
with specific and shared theme functions.
First we create our theme
folder, then create files for our theme functions and our ThemeProvider
high order component:
// At project root, create the theme folder
$ mkdir src/theme // Create a project folder
$ mkdir src/theme/project1 // Create a shared utils folder
$ mkdir src/theme/shared// Add Colour theme
$ touch src/theme/project1/color.ts// Add Font Sizes theme
$ touch src/theme/project1/fontSize.ts// Exports themes
$ touch src/theme/project1/theme.ts// Add Font family theme
$ touch src/theme/shared/fontFamily.ts// Add breakpoint theme
$ touch src/theme/shared/breakpoint.ts// Theme Provider HOC
$ touch src/theme/ThemeProvider.ts
Modify the color.ts
file
// src/theme/project1/color.tsconst colors: {[index: string]: string } = {
red: '#e94b35',
blue: '#337ab7',
blueHover: '#286090',
green: '#3c763d',
greenHover: '#2b542c',
orange: '#ff9900',
white: '#FFFFFF',
};
const color = (colorName: string): string => {
return colorName ? colors[colorName] : 'inherit';
};
export default color;
Next, modify the fontSize.ts
file
// src/theme/project1/fontSize.tsexport const fontSizes: {[index: string]: string } = {
xs: '0.83125rem',
s: '1rem',
m: '1.2rem',
l: '1.44rem',
xl: '1.725rem',
super: '6.25rem',
};
const fontSize = (size: string): string => {
return size ? fontSizes[size] : '';
};
export default fontSize;
Also, modify the shared fonts fontFamily.ts
file
// src/theme/shared/fontFamily.ts
interface IFontFamilies {
[index: string]: {
font: string;
fallback: string;
}
}
const fontFamilies: IFontFamilies = {
Montserrat: { font: 'Montserrat', fallback: 'Helvetica, Arial' },
Anton: { font: 'Anton', fallback: 'Helvetica, Arial' },
Trebuchet: { font: 'Trebuchet MS', fallback: 'Helvetica, Arial' },
};
export default (family: string) => {
return family ? `'${fontFamilies[family].font}', ${fontFamilies[family].fallback}, sans-serif` : 'inherit';
};
And, add some custom breakpoints in breakpoint.ts
// src/theme/shared/breakpoint.ts
export const ScreenSizes: {[index: string]: number } = {
small: 740,
medium: 1024,
large: 1440,
};
export default (size: string): string => {
return size ? `(min-width: ${ScreenSizes[size]}px)` : 'inherit';
};
We need to export our project themes:
// // src/theme/project1/theme.tsimport color from './color';
import fontSize from './fontSize';
import fontFamily from '../shared/fontFamily';
import breakpoint from '../shared/breakpoint';
export default {
color,
fontSize,
fontFamily,
breakpoint
};
We can specify and customise which themes we want for each project.
Next, we export our ThemeProvider.ts
// src/theme/ThemeProvider.tsimport { ThemeProvider } from 'styled-components';
export default ThemeProvider;
Styleguide
We also need to define a layout for our Styleguide. So, we create a ThemeWrapper
component to serve as a top level wrapper component and customises the layout.
// Create styleguide folder
$ mkdir src/styleguide$ touch src/styleguide/ThemeWrapper.tsx
And we enter the following:
// src/styleguide/ThemeWrapper.tsximport * as React from 'react';
import { ThemeProvider } from 'styled-components';
import theme from '../theme/project1/theme';
const ThemeWrapper: React.FC = ({ children}) => {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
};
export default ThemeWrapper;
The ThemeProvider
component accepts a theme
prop, which it passes it down to all child components.
Next, we modify our styleguide.config.js
and update the styleguideComponents
prop to our new ThemeWrapper
component, enabling a customised layout:
//styleguide.config.js
styleguideComponents: {
Wrapper: path.join(__dirname, 'src/styleguide/ThemeWrapper')
}
Next we modify the script commands in our package.json
file
"scripts": {
"styleguide": "styleguidist server",
"styleguide:build": "styleguidist build",
....
},
Running yarn run styleguide
will start the style guide dev server, while yarn run styleguide:build
builds a production-ready HTML version.
Components
In structuring the components, I have adopted the Atomic Design system proposed by Brad Frost.
// Create the components folder
$ mkdir src/components// Create the Atoms folder
$ mkdir src/components/Atoms// Create our Atomic components
$ mkdir src/components/Atoms/Text
$ touch src/components/Atoms/Text/Text.tsx$ mkdir src/components/Atoms/RichText
$ touch src/components/Atoms/RichText/RichText.tsx// Create the Molecules folder
$ mkdir src/components/Molecules// Create our Molecule component
$ mkdir src/components/Molecules/Section
$ touch src/components/Molecules/Section/Section.tsx
We modify Text.tsx
atomic component as follows:
Next, we modify RichText.tsx
atomic component
Next, we create our Section.tsx
molecule component. Molecules are a combination of two or more Atomic components
Styleguidist handles documentation using the JSDoc comment blocks in the components and also picks up props from each propTypes
declaration and displays them in a table.
To view the styleguide library locally, we run yarn run styleguide
to start styleguide dev server and we can view it at http://localhost:6060/
We can see our Introduction
but not our new components.
This is because Styleguidist will look for a README file such as a Readme.md
or Text.md
or RichText.md
within the respective components folder in order to display them. You can read more about Styleguidist documentation.
We need to create README files for our new components:
// Create Text component Readme file
$ touch src/components/Atoms/Text/Text.md// Create RichText component Readme file
$ touch src/components/Atoms/RichText/RichText.md
Then add a few examples such as the following to the Text.md
:
And alsoRichText.md
After a hot reload, Styleguidist will generate the components from the markup:
Testing
Styleguidist uses Jest with Enzyme for testing.
To add tests to our library, we need to install some dependencies:
$ yarn add jest-css-modules-transform
// Install jest and dependencies
$ yarn add -D jest jest-styled-components babel-jest// Allows us to render React components outside the DOM
$ yarn add react-test-renderer @types/react-test-renderer
Next, we need to add some tests for our components. First, we create a higher order shallow wrapper component:
// Create hoc folder
$ mkdir src/hoc// Create renderer component
$ touch src/hoc/shallowWithTheme.tsx
Update the shallowWithTheme
component as follows:
Next, we create our test files
// Create test files at component levels
$ touch src/components/Atoms/Text/Text.test.tsx$ touch src/components/Atoms/RichText/RichText.test.tsx$ touch src/components/Molecules/Section/Section.test.tsx
And we enter the following to theText.test.tsx
, RichText.test.tsx
and Section.test.tsx
files:
We need to update our package.json
with our test commands:
"scripts": {
...
"test": "yarn run jest --collectCoverage",
"test:watch": "yarn run jest --watch",
"test:update": "yarn run jest -u",
"test:no-warnings": "yarn run jest --watch --silent",
...
},
Running yarn run test
will run the full suite of tests; yarn run test:watch
watches test files for changes, yarn run test:update
updates our tests and yarn run test:no-warnings
runs tests in silent mode.
Linting
It is important to have a clear and consistent coding convention. ESLint makes it possible to identify, report and fix any inconsistencies and bugs in our code during development.
To add linting to our project, we need to install a few dependencies:
// Install ESLint and plugins
$ yarn add -D eslint eslint-plugin-react eslint-plugin-react-hooks// Lint styleguide support
$ yarn add -D eslint-config-airbnb eslint-plugin-import@^2.21.2 eslint-plugin-jsx-a11y@^6.3.0// Typescript support
$ yarn add -D @typescript-eslint/eslint-plugin @typescript-eslint/parser// Install Prettier and plugins
$ yarn add -D prettier eslint-plugin-prettier eslint-config-prettier// Installs husky and lint
$ yarn add -D husky lint-staged
We also installed Prettier which forces code formatting; Lint-staged which ensures that linting is run on files before they are committed; Husky which helps makes git hooks easier.
We then need to set up a configuration file for ESLint:
$ npx eslint --init
This generates a configuration file .eslintrc.js
. I have added some modifications with our plugins and rules:
Next, we need to create an .eslintignore
file which tells ESLint which directories or files to ignore and add the following:
node_modules
dist
tests
coverage
styleguide.config.js
src/**/*.test.tsx
src/**/*.md
src/serviceWorker.ts
Next, we need to also update our package.json
for by adding our lint command to the script field and adding new fields for husky
and lint-staged
:
"scripts": {
...
"lint": "eslint 'src/**/*.{js,ts,tsx}' --quiet --fix"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx,json,css,scss}": [
"prettier --single-quote --write",
"eslint --fix"
]
}
We can run yarn run lint
to lint test our files. Also, every changed file to will also be lint tested before it is committed.
Production
In order to use our library as npm module, we need to export our themes and components and compile our typescript files into plain Javascript before we can publish as a module and import into our projects.
First, we create our export file src/index.ts
and add the following:
Next, we need to install the following modules :
// TypeScript execution for node
$ yarn add -D ts-node// Typescript loader for webpack
$ yarn add -D ts-loader
And update our script field inpackage.json
with our postinstall
and build
command.
"scripts": {
......
"postinstall": "yarn run build",
"build": "NODE_ENV=production tsc",
......},
This means that everytime we run yarn install
, the build
command will be triggered, creating a new build of our project. Next, we also need to modify our Typescript configuration file tsconfig.json
to specific our source foldersrc
and compiled destination dist
folder and also which directories or files to exclude from typechecks.
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"declaration": true,
"sourceMap": true,
"skipLibCheck": true,
"esModuleInterop": true,
/* Redirect output structure to the directory. */
"outDir": "./dist",
/* Specify the root directory of input files. */
"rootDir": "./src",
/* Do not emit comments to output. */
"removeComments": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noImplicitAny": false,
// "noEmit": true,
"jsx": "react"
},
"include": [
"src"
],
"exclude": [
"node_modules",
"src/__tests__",
"**/*.test.tsx",
"**/*.test.ts",
"dist"
]
}
Next, we run yarn run build
to generate compiled javascript files in the destination dist
folder.
We also need to run yarn run styleguide:build
to generate a production-build of a HTML-static library in the styleguide
folder. Your project structure should now look like this:
Next we updatepackage.json
by adding new fields to point to the compiled source as below:
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts"
Publishing
Next, before we can publish our library, we need to also create a .npmignore
file, which tells NPM which directories or file to ignore. We then add the following:
src
node_modules
tests
.idea
coverage
And we can run npm publish
, which publishes the library on NPM. You need to have an account on NPM before you can publish. If you don’t have an account, you can create one and it is free. Also, the name of your library / module must be unique on NPM. In this case, I named the library rcl-demo
on NPM.
Continuous Integration and Development, Versioning and Publishing
Next, we automate our builds, tests and deployments using Github and CircleCI . You can read more about how to set up CircleCI and what Continuous Integration and Development means. You can choose whichever CI/CD platforms you prefer.
First, we push our remote repository on Github and then look for and set up the project on the CircleCI Dashboard.
I have opted to manually add the configuration file .circleci/config.yml
to my project:
// Create CircleCI configuration file at the root of project
$ mkdir .circleci
$ touch .circleci/config.yml
We will also be using Semantic Release to automate version management and publishing of our package to NPM. To do that, we need to install the module and it’s CLI:
// Install Semantic release
$ yarn add semantic-release-cli
$ yarn add -D semantic-release
By default, Semantic Release expects commits to be in the conventional GIT commit format likefeat: add feature X
or fix: fix bug Y
that perform minor
and patch
version bumps respectively.
The CircleCI configuration is triggered on every push to the remote repository on GitHub and this also triggers a release as a part of the workflow:
Next, we need to setup our authentication tokensGH_TOKEN
/ GITHUB_TOKEN
and NPM_TOKEN
environmental variables in the CircleCI project setup. Read Creating a personal access token on how to create an access token on GitHub. Also, you need to have an account on NPM to create an access token.
After renaming the library to rcl-demo
, we trigger another deployment
We also need to update the README.md
file as below:
Usage
To use our library in any project, we simply need to install by running yarn add rcl-demo
or npm install rcl-demo
.
Next we need to import the. ThemeProvider
and theme
as a high order component wrapper in our application:
import { ThemeProvider, theme } from 'rcl-demo';<ThemeProvider theme={theme}>
<div className="app">
...
</div>
</ThemeProvider>
And we can import any component and use anywhere in the project
import { Text } from 'rcl-demo';<Text tag="p" size="xl" family="Anton">
My paragraph xl and font Anton
</Text>
Preview
To deploy the static pages on Netlify, you need login / signup and import the project from Github. Next, we need to update the build
command and publish
directory
Clicking on theDeploy site
button should trigger the deployment:
With Netlify, you can also customise the settings for the static site and domain.
As soon as deployment is finished, we can preview our static library:
Conclusion
Having a component library is definitely the way to go if you want to have sharable and reusable components and prevent code duplication across different projects. It also means we can also fix bugs or add new features to our components with less effort.
There are various frameworks that also help create your own system or provide you with reusable components right out-of-the-box like Material UI, React Bootstrap, Fluent UI and Semantic UI.
I have included the Github repository with the current implementation in this article. Feel free to clone, fork or star it if it helps you.