Creating a react-bootstrap Typescript Library

Posted Tuesday, January 5, 2021.

I had a lot of requirements for this project, and it took a great deal of trial and error to get all of them working in tandem. Here is how I did it.

Create new NPM project and Typescript config

mkdir component-kit
cd component-kit
npm init -y
tsc --init

Add dependencies

I prefer yarn but feel free to add dependencies with npm (or edit package.json directly)

Peer Dependencies:

yarn add -P \
react \
react-dom \
react-bootstrap

Dev Dependencies:

yarn add -D \
@babel/core \
@babel/preset-env \
@rollup/plugin-commonjs \
@rollup/plugin-node-resolve \
@storybook/addon-actions \
@storybook/addon-essentials \
@storybook/addon-links \
@storybook/addon-postcss \
@storybook/addons \
@storybook/builder-webpack5 \
@storybook/manager-webpack5 \
@storybook/node-logger \
@storybook/react \
@storybook/theming \
@testing-library/jest-dom \
@testing-library/react \
@types/jest \
@types/react \
@types/react-dom \
@typescript-eslint/eslint-plugin \
@typescript-eslint/parser \
autoprefixer \
babel-loader \
css-loader \
eslint \
eslint-config-prettier \
eslint-plugin-prettier \
eslint-plugin-react \
fork-ts-checker-webpack-plugin \
node-sass \
postcss \
postcss-flexbugs-fixes \
prettier \
react \
react-docgen-typescript \
react-dom \
rollup \
rollup-plugin-peer-deps-external \
rollup-plugin-postcss \
rollup-plugin-typescript2 \
sass-loader \
style-loader \
tslib \
typescript \
webpack

Folder Setup

We will make a library with a single component TestComponent. Create a folder structure to look like this:

.
├── .storybook
│ ├── main.js
│ ├── theme.js (optional)
│ ├── manager.js (optional)
│ └── preview.js
├── dist/
├── src
│ ├── TestComponent
│ │ ├── index.ts
│ │ ├── TestComponent.md
│ │ ├── TestComponent.scss
│ │ ├── TestComponent.tsx
│ │ └── TestComponent.types.ts
│ ├── index.scss
│ └── index.ts
├── .gitignore
├── .eslintrc
├── .prettierrc
├── .gitignore
├── package.json
├── README.md
├── rollup.config.js
└── tsconfig.json

Adjust package.json to look like this (adjusting personal details):

{
"name": "@hiimtmac/component-kit",
"version": "1.0.0",
"description": "Component Library",
"author": "hiimtmac",
"main": "dist/index.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "rm -rf ./dist/* && rollup -c",
"yalc": "yalc push",
"test": "jest",
"test:watch": "jest --watch",
"storybook": "start-storybook -p 6006",
"storybook:export": "build-storybook"
},
"dependencies": {
...
},
"peerDependencies": {
...
},
"devDependencies": {
...
}
}

Adjust tsconfig.json to look like this:

{
"compilerOptions": {
/* Basic Options */
"target": "es5",
"module": "esnext",
"lib": ["es6", "dom", "es2016", "es2017"],
"jsx": "react",
"declaration": true,
"declarationDir": "dist",
"sourceMap": true,
"rootDir": "src",

/* Strict Type-Checking Options */
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,

/* Additional Checks */
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,

/* Module Resolution Options */
"moduleResolution": "node",
"esModuleInterop": true,

/* Advanced Options */
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": [
"node_modules",
"dist",
"storybook-static",
"src/**/*.stories.tsx",
"src/**/*.test.tsx"
]
}

Adjust rollup.config.js to look like this:

import peerDepsExternal from "rollup-plugin-peer-deps-external";
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "rollup-plugin-typescript2";
import postcss from "rollup-plugin-postcss";
import autoprefixer from "autoprefixer";
import path from "path";
import packageJson from "./package.json";

export default {
input: "./src/index.ts",
output: [
{
file: packageJson.main,
format: "cjs",
sourcemap: true,
},
{
file: packageJson.module,
format: "esm",
sourcemap: true,
},
],
plugins: [
peerDepsExternal(),
resolve(),
commonjs(),
typescript(),
postcss({
extract: true,
sourceMap: true,
extensions: [".scss", ".css"],
namedExports: true,
plugins: [autoprefixer],
minimize: true,
use: [
[
"sass",
{
includePaths: [path.resolve("node_modules")],
},
],
],
}),
],
};

Peer dependencies caused me a lot of trouble using webpack (specifically when using react-bootstrap and it gave me a lot of errors about multiple copies of react when I would import my library). Make sure to be careful what is a true dependency vs what should be a peer dependency.

Create your component(s)

We will make a very simple component that wraps the react-boostrap loader, either showing a spinner with optional text if prop variable is in loading state, or the content if prop variable is not in loading state (image below shows loading state).

Example component render

First will be the TestComponent.types.ts file where we will declare our props.

import { ReactNode } from "react";

export type TestComponentProps = {
isLoading: boolean;
message?: string;
children?: ReactNode;
};

Then we will declare the component in TestComponent.tsx.

import React from "react";
import { Spinner } from "react-bootstrap";
import { TestComponentProps } from "./TestComponent.types";
import "./TestComponent.scss";

export const TestComponent = (prop: TestComponentProps) => {
const Loader = () => {
return (
<div style={{ textAlign: "center" }}>
<Spinner animation="border" role="status">
<span className="sr-only">Loading...</span>
</Spinner>
{!!prop.message && <p>{prop.message}</p>}
</div>
);
};

if (prop.isLoading) {
return <Loader />;
} else {
return <React.Fragment>{prop.children}</React.Fragment>;
}
};

Adjust any stylings in TestComponent.scss.

We will be using Storybook JS to preview our components (setup in later section). Adjust TestComponent.stories.tsx.

import React from "react";
import { TestComponent } from "./TestComponent";

export default {
title: "TestComponent",
};

export const Primary = () => <TestComponent isLoading={true} message="We are loading" />;
export const Secondary = () => (
<TestComponent isLoading={false}>
<p>Done loading!</p>
</TestComponent>
);

This will declare two separate previews for this component which you can select on the storybook nav like so:

Storybook nav for component

Index Setups

Setup proper imports in index.ts files:

/src/TestComponent/index.ts should include an export for our component:

export { TestComponent } from "./TestComponent";

src/index.ts should include an export for our component folder, and an import for our root index.scss file:

import "./index.scss";
export { TestComponent } from "./TestComponent";

Any global styles can go into the src/index.scss file.

This index work might seem useless but becomes more helpful when you have lots of components and dont have to do longer path resolving in your root index.ts like export { CoolButton } from "./components/buttons/cool/CoolButton"

Storybook Config

Adjust /.storybook.main.js to have the following:

const path = require("path");

module.exports = {
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-postcss"],
typescript: {
check: true,
checkOptions: {},
reactDocgen: "react-docgen-typescript",
reactDocgenTypescriptOptions: {
propFilter: (prop) => ["label", "disabled"].includes(prop.name),
},
},
core: {
builder: "webpack5",
},
babel: async (options) => ({
...options,
plugins: [
["@babel/plugin-proposal-class-properties", { loose: true }],
["@babel/plugin-proposal-private-methods", { loose: true }],
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
],
}),
webpackFinal: async (config, { configType }) => {
config.module.rules.push({
test: /\.scss$/,
use: ["style-loader", "css-loader", "sass-loader"],
include: path.resolve(__dirname, "../src"),
});

return config;
},
};

In order to have our global styles show in the previews we need to import our main sass file. Adjust /.storybook/preview.js to have the following:

import "../src/index.scss";

export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};

If you want a custom theme, declare it in ./storybook/theme.js:

import { create } from "@storybook/theming";

export default create({
base: "dark",
brandTitle: "hiimtmac",
brandUrl: "https://hiimtmac.com",
brandImage: "https://assets.website-files.com/hiimtmac.png",
colorPrimary: "hotpink",
colorSecondary: "deepskyblue",
});

And then import it in ./storybook/manager.js:

import { addons } from "@storybook/addons";
import theme from "./theme";

addons.setConfig({
theme: theme,
});

You can then run and preview your components by running yarn storybook. This is helpful as you don't need a test project to import the library for previewing.

Storyboard output

If you do want to import it into a test project, I would recommend using Yalc.

Building

Finally, you can build the library for distribution with yarn build. This will output all build files in the /dist folder. From here you can publish to NPM or github.

Once you've pushed it to a repo, you can add this to your projects with yarn (or npm), just like any other dependency:

yarn add @hiimtmac/component-kit

And then use it in your app!

import React, { useState } from "react";
import { TestComponent } from "@hiimtmac/component-kit";

export const Test = () => {
const [loading, setLoading] = useState(true);

return <TestComponent isLoading={networking}>Loading is over!</TestComponent>;
};

In Closing

Types are life. Use Typescript. It's as close as I can find to Swift for web development. In a future post, I will detail how to adjust this project to use a private github repo as the npm repo, and how to pull from it in projects.


Tagged With: