【React】UIコンポーネントライブラリの作成・公開する方法

# はじめに

こんにちは、エンジニアのkeitaMaxです。

今回は、ReactのUIライブラリを作成して公開する方法についてを記事にさせていただきます。

以下の記事を参考にさせていただきます。

https://qiita.com/yohei_nakamura/items/fd1f2f418f24d4dea503

# プロジェクト作成

以下コマンドでReactのプロジェクトを作成します。

```
npm create vite@latest
```

色々聞かれると思うので、以下のように答えます。
```
develop % npm create vite@latest
Need to install the following packages:
create-vite@5.3.0
Ok to proceed? (y) y
✔ Project name: … react-ui-components-example
✔ Select a framework: › React
✔ Select a variant: › TypeScript

Scaffolding project in /Users/niheikeita/develop/react-ui-components-example...

Done. Now run:

  cd react-ui-components-example
  npm install
  npm run dev

develop % 
```

# StoryBookのインストール

StoryBookを以下コマンドでインストールします。

```
npx storybook@latest init　
```

```
npm i vite-plugin-dts
npm i @types/node
```

# TailwindCSSを使えるようにする

今回はTailwindCSSを使用するので以下のコマンドでインストールします。

```
npm install -D tailwindcss postcss autoprefixer
```

すると`tailwind.config.js`ファイルが作成されるので、以下のように修正します。

```js:tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
//追加↓
  content: [
    "./src/**/*.{js,jsx,ts,tsx}",
  ],
//追加↑
  theme: {
    extend: {},
  },
  plugins: [],
}
```

`indec.css`ファイルに以下を追加します。

```css:index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

# コンポーネントの作成

`src/components`というフォルダを作成し、その配下に`Button`ファイルを作成します。
その配下に`index.tsx`と`index.stories.tsx`ファイルを作成します。
```
src
└──components
　   └── Button
　   　　　　　　　　├── index.tsx
    　　　　　　　└── index.stories.tsx
```

```tsx:index.tsx

import React from "react"

type Props = {
  children: React.ReactNode,
  variant: 'default' | 'pink'
}

export const Button = React.memo<Props>(function Button({
  children,
  variant = 'default'
}) {
  const color = (() => {
    switch (variant) {
      case 'default':
        return 'bg-white text-pink-500'
      case 'pink':
        return 'bg-pink-500 text-white'
    }
  })()

  return (
    <button className={color + ' rounded-full px-4 py-2 font-bold transition-transform duration-200 hover:scale-105'}>
      {children}
    </button>
  )
})

```

```tsx:index.stories.tsx
import { Meta, StoryObj } from '@storybook/react'
import { Button } from '.'

const meta: Meta<typeof Button> = {
  title: 'components/Button',
  component: Button,
  tags: ['autodocs'],
}
export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {
  render() {
    return (
      <div className='grid gap-2 bg-gray-100 p-10'>
        <Button variant={'default'} >default</Button>
        <Button variant={'pink'}>pink</Button>
      </div>
    )
  },
}

```

こんな感じでボタンのコンポーネントを作成してみました。

StoryBookを起動してみると以下のようにTailwinCSSが効いていることがわかります。

![スクリーンショット 2024-07-09 13.01.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/6ceeb4a8-ef30-c3cc-8374-cb3925947690.png)

`src/stories`ファイルはいらないので削除してしまいます。

また、`src/index.tsx`を作成し以下のようにします。
```tsx:index.tsx
import './index.css'

export * from "./Button"
```

# 各種設定

参考下の記事と同様に以下のようにします。

```ts:tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "allowSyntheticDefaultImports": true,
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    "allowJs": false,
    "esModuleInterop": false,
    "moduleResolution": "Node"
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}
```
```ts:tsconfig.node.json
{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts", "package.json"]
}
```

```ts:vite.config.ts

import { resolve } from "path";
import react from "@vitejs/plugin-react";
import autoprefixer from "autoprefixer";
import tailwindcss from "tailwindcss";
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    dts({
      insertTypesEntry: true,
    }),
  ],
  css: {
    postcss: {
      plugins: [tailwindcss, autoprefixer],
    },
  },
  build: {
    lib: {
      entry: resolve(__dirname, "src/index.ts"),
      name: 'index',
      fileName: 'index',
    },
    rollupOptions: {
      external: ["react", "tailwindcss"],
      output: {
        globals: {
          react: "React",
          tailwindcss: "tailwindcss",
        },
      },
    },
  },
});

```

`package.json`は以下を追加します。

```json
  "main": "./dist/index.umd.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "license": "MIT",
  "exports": {
    ".": {
      "types": "./dist/index.umd.cjs",
      "import": ["./dist/index.js","./dist/style.css"],
      "require": "./dist/index.d.ts"
    }
  },
  "files": [
    "dist"
  ],
```

また`package.json`の`script`にいいかを追加します。

```json
build": "tsc && vite build",
```

また以下をfalseに修正します。

```json
"private": false,
```

# 公開する

まず以下コマンドでbuildします。

```
npm run build
```

以下のコマンドでnpmにログインをします。
まだnpmのアカウントがない方は作成してください。

https://www.npmjs.com/

```
npm login
```

そして以下コマンドで公開するためにpublishします。
```
npm publish
```

npmにログインをしてみてみると以下のようにライブラリが公開されているのがわかります。

![スクリーンショット 2024-07-09 16.12.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/5d1e193c-51ba-5851-3f7e-b80f547e5dd1.png)

![スクリーンショット 2024-07-09 16.12.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/ea5493b3-0472-6ed9-4847-3f77d81475d9.png)

https://www.npmjs.com/package/react-ui-components-example

# おわりに

この記事での質問や、間違っている、もっといい方法があるといったご意見などありましたらご指摘していただけると幸いです。

最後まで読んでいただきありがとうございました！

# 参考

https://tailwindcss.com/docs/guides/create-react-app

https://qiita.com/yohei_nakamura/items/fd1f2f418f24d4dea503

# 次の記事

https://qiita.com/keitaMax/items/bf74d1a472bf4cae3d86