Build A React UI Library (WIP)

Published on 3/1/2024

Table of Contents

This article is a collection of notes stemming from my Build React UI Youtube Series. These notes aren’t meant to serve as a standalone solution but rather as notes to use while following along with the series – or to refer back to.

⚠️ These notes are very much Work In Progress

Setup UI

For the first video, we setup the React UI library by using vite library mode.

Steps 1, install Vite:

Vite Build Guide

npm create vite@latest

React + TypeScript

cd [into project]
npm install && npm run dev # runs the project

Step 2, setup library mode with config:

import { defineConfig } from 'vite'
import { resolve } from 'path'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  build: {
    // library entry (src/main.tsx)
    lib: {
      entry: resolve(__dirname, 'lib/main.tsx'),
      name: 'react-ui-library',
      fileName: 'react-ui-library' // this will add extensions automatically
    },
    // import react and react-dom as external
    rollupOptions: {
      external: ['react', 'react-dom', 'react/jsx-runtime'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
          'react/jsx-runtime': 'react/jsx-runtime'
        }
      }
    }
  }
})

Follow rest of guide https://vitejs.dev/guide/build:

  1. create lib/main
  2. create lib/components/Button
  3. update package.json and make sure to use the correct lib name
  4. test build

Package.json:

{
  "name": "react-ui-library",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "files": ["dist"],
  "main": "dist/react-ui-library.cjs",
  "module": "dist/react-ui-library.js",
  "exports": {
    ".": {
      "import": "./dist/react-ui-library.js",
      "require": "./dist/react-ui-library.umd.cjs"
    }
  },
  "scripts": {
    "dev": "vite",
    "prebuild": "rm -rf build",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview",
    "prepublish": "npm run build"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.55",
    "@types/react-dom": "^18.2.19",
    "@typescript-eslint/eslint-plugin": "^6.21.0",
    "@typescript-eslint/parser": "^6.21.0",
    "@vitejs/plugin-react": "^4.2.1",
    "eslint": "^8.56.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.5",
    "typescript": "^5.2.2",
    "vite": "^5.1.0"
  }
}

Step 3, ts config paths:

https://www.npmjs.com/package/vite-tsconfig-paths

install:

npm install vite-tsconfig-paths

Update Vite and TS configs:

TSConfig should reference the paths – directly path to main.tsx

https://www.typescriptlang.org/tsconfig#paths

Step 4, test it!

  • import a component
  • render it!
  • preview it using npm run dev

Tailwind and Storybook

Step 1: install tailwind

official guide

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    // reference the library _only_
    "./lib/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Step 2: create ./lib/index.css

@tailwind base;
@tailwind components;
@tailwind utilities;

Import it in main.tsx

Step 3: test Tailwind!

  • add a color to the button
  • publish it
  • install it and test it in a separate project

Step 4: Storybook

https://storybook.js.org/blog/storybook-for-vite/

Initialize storybook for vite

npx sb init --builder @storybook/builder-vite

Run it for the first time:

npm run storybook

Step 5: Setup storybook for our usecase

Delete ./src/stories

Adjust the storybook/main.ts file

import type { StorybookConfig } from "@storybook/react-vite";

const config: StorybookConfig = {
  // our lib paths
  stories: ["../lib/**/*.mdx", "../lib/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-onboarding",
    "@storybook/addon-interactions",
  ],
  framework: {
    name: "@storybook/react-vite",
    options: {},
  },
  docs: {
    autodocs: "tag",
  },
};
export default config;

Start storybook

npm run storybook

Step 6: Add First Story

import type { Meta, StoryObj } from '@storybook/react'

import { Button } from './Button'

const meta: Meta<typeof Button> = {
  component: Button,
}

export default meta

type Story = StoryObj<typeof Button>

export const Default: Story = () => <Button>Click me</Button>

It should work! :D

Step 7: Add Tailwind to Storybook

I think this is where the complexity starts to get ridiculous and it’ll get even more ridiculous. We’re going to add Tailwind to Storybook.

This steps feels like crossing wires but I don’t know a better solution yet. Import tailwind.css in .storybook/preview.js

import '../lib/tailwind.css'

Export Styles And Types

In this video, we will export CSS and Types for our React UI Library project for target applications to use when importing Cottage UI.

Resources:

Styles and types aren’t getting exported yet which hampers our ability to use this UI library. Let’s go ahead and add that.

Reinstall command

Add this command to your package.json for easy reinstall:

"reinstall": "rm -rf node_modules && npm install"

Styles

Add the style export and mark side effects as false in package.json.

"exports": {
	"./dist/style.css": "./dist/style.css"
},
"sideEffects": false

Target app needs to import it:

import 'cottage-ui/dist/style.css'

Why?

Could not get things to work correctly with other solutions and this one seemed to make the most sense – it imports TailwindCSS. This really depends on how you want to structure your CSS.

Since this is supposed to be an internal library, I prefer the simplicity of the solution over having to import an additional file

Types

https://www.npmjs.com/package/vite-plugin-dts

npm install --save-dev vite-plugin-dts

add plugin:

import dts from 'vite-plugin-dts'
plugins: [dts({ rollupTypes: true })]

add include into typescript:

includes: ['lib']

Run it!

How about tree-shaking?

We’ll handle that in the future.