Next.js + Tailwind CSS + TypeScript でコンポーネント書いてみる – Storybookとtailwindcss-classnamesを添えて


こんにちは、mimiです。

なんかこの、Next.js + Tailwind CSS + TypeScriptの組み合わせの短い名前が欲しい今日この頃です。ちんぷんかんぷんなフランス料理みたいだなと思って今日はこんなタイトルにしてみました。

ここの処、ふわーっとした感じでこの辺りの技術をつまみ食いしておりましたが、最近ようやくがっつり開発に使い出しています。

テスト駆動でコンポーネントをどうやって作っていくのがいいのか、まだまだ手探りなのですが、Tailwind CSSでのStorybook導入がちょっとトリッキー(と言っても指示通りに書けば動くんですけど)なのと、tailwindcss-classnamesがいい感じにTailwindのツラい感じを解消してくれて、大分心地よく書けてるようになったので、その辺りを合わせてメモしておこうと思います。重ねて書きますが、トライ・アンド・エラー真っ最中のメモなので悪しからず。

Storybook導入

トリッキーといっても、大体の問題はすでに踏み抜いて解決している先達が世界を探せば居るので、そのブログ記事を有り難く共有いたします。

How to set up Storybook with Next.js and Tailwind CSS | theodorusclarence.com

PostCSSを8にしたりwebpackを5以上にしたり細かい設定が諸々いるのですが、記事の通りに書いたら動きます。

私の場合は、導入が後だったので、

Error: PostCSS plugin tailwindcss requires PostCSS 8

というエラーが出て、postcssが8系じゃなかったので入れ直しました。

yarn remove tailwindcss postcss autoprefixer -D
yarn add tailwindcss postcss autoprefixer -D

したら"postcss": "^8.4.13"になってstorybookが立ち上がり、StorybookにTailwindが反映できるようになりました🎉

tailwindcss-classnames導入

さて、それじゃあ本気でコンポーネント化するぜいと思ってもりもりパターン増やしたりしてゴリゴリTailwindを書いているとですね、最初は

className="p-2 bg-white rounded border border-gray-700"

ぐらいだったのがhoverなどの諸々をちゃんと書いているとめちゃくちゃ長くなりますよね。それがTailwindですよね(?)
でもせっかくTypeScriptなのにツラいじゃない?イケてる最新?コーディング??(なのか?)のはずなのに、インラインスタイル並みの視認性の辛さがありますよね。

それをいい感じに分割して整理して書けて、追加したり出来て、且つタイポとかをちゃんと見て叱ってくれるのがmuhammadsammy/tailwindcss-classnames: Functional typed classnames for TailwindCSSです。

npm install tailwindcss-classnames
// または
yarn add tailwindcss-classnames

でインストールします。

試しにDefaultとPrimaryのスタイルがある超シンプルなボタンのコンポーネントを書いてみます。

Button.ts(Tailwind入れる前)

import { ReactNode } from 'react'
import { classnames } from 'tailwindcss-classnames'

type buttonProps = {
  children: ReactNode
  type?: 'button' | 'submit' | 'reset'
  disabled?: boolean
  loading?: boolean
  style?: 'Default' | 'Primary'
}

export const Button = (button: buttonProps) => {
  const { type = 'button' } = button

  const baseStyle = classnames(
      // ここにbaseのスタイル
  )
  const defaultStyle = classnames(
      baseStyle,
      // ここにdefaultのスタイル
  )
  const primaryStyle = classnames(
      baseStyle,
      // ここにprimaryのスタイル
  )
  return (
    <button
      type={type}
      className={button.style === 'Primary' ? primaryStyle : defaultStyle}
      disabled={button.disabled ? true : false}
    >
      {button.children}
    </button>
  )
}

上のような感じで、classnamesの中にTailwindを書いていくことが出来ます。(この書き方が良いか分かりません、ただの一例です)例えば下記のように書けます。

Button.ts(Tailwind 追加後)

import {
  backgroundColor,
  borderColor,
  borderRadius,
  borderWidth,
  classnames,
  dropShadow,
  padding,
  textColor,
  transitionProperty,
} from 'tailwindcss-classnames'

type buttonProps = {
  children: ReactNode
  type?: 'button' | 'submit' | 'reset'
  disabled?: boolean
  loading?: boolean
  style?: 'Default' | 'Primary'
}

export const Button = (button: buttonProps) => {
  const { type = 'button' } = button

  const baseStyle = classnames(
    padding('p-2'),
    borderWidth('border'),
    borderRadius('rounded'),
    dropShadow(
      'drop-shadow-sm',
      'hover:drop-shadow-xl',
      'disabled:drop-shadow-none'
    ),
    transitionProperty('transition-all')
  )
  const defaultStyle = classnames(
    baseStyle,
    textColor('text-black', 'disabled:text-gray-400'),
    borderColor('border-gray-700', 'disabled:border-gray-400'),
    backgroundColor('bg-white')
  )
  const primaryStyle = classnames(
    baseStyle,
    textColor('text-white'),
    backgroundColor(
      'bg-indigo-700',
      'hover:bg-indigo-800',
      'disabled:bg-indigo-300'
    )
  )
  return (
    <button
      type={type}
      className={button.style === 'Primary' ? primaryStyle : defaultStyle}
      disabled={button.disabled ? true : false}
    >
      {button.children}
    </button>
  )
}

カスケードするのがCSSじゃい脳から抜け出せない老害予備軍としてはこの書き方心地いいなーと思いました。backgroundColor とかの名前も、VS CodeにTailwind CSS IntelliSense – Visual Studio Marketplaceを入れていれば補完してくれるし勝手にimportに追加してくれるので、CSSを大体理解していたら、たぶんあのクラスはこの辺かなと適当に打ち込めばいけると思います。

ちなみに、今は上の書き方も変えて、switch文にしてます。タダの例なのでコピペはおすすめしません。
全部これにしている訳ではなく、今の処componentの中だけで活用中。

Tailwindいいかも…?

Tailwindって、わーっと作ってる最中につけたクラスもサクサク捨てられてガチッと固めたければコンポーネント化したり、@applyしたりして、既存のスタイル弄りたくなったらthemeをextendしていって…という、スケールさせやすいって言ったら良いのか、いろんな作り方に対応できて凄く良い気がしてきています。ただ、CSS初心者にはあんまりオススメするのも微妙というか一応基本は学んだ人が使う方が良いと思うけれど、まあ表面的に使う使い方も全然イケるのも良い点なのかもしれない。CSSはそのまま使うには人類には早すぎるので。

コンポーネント開発のフロー(暫定)

というわけでこれらを導入してコンポーネントを書くフローについてもメモしておこうと思います。

今やっている開発では、Next.js + Tailwind CSS + TypeScript にすでに Jest が乗っかっており、ユニットテストはそれで書いているのですが、見た目チェックをしたくてStorybookも導入してみました。too muchなのかな?両方あるほうが今の処、私にはいい感じです。

ユニットテストはこの(自分の) JavaScript のユニットテストの書き方が凄くいいなーと思っていてマネッコしています。Buttonコンポーネントを作るなら、Button.test.tsで最初の原型をざっと書いてテストを回してから、Button.tsにコピペする、というやり方が凄く安心感があって好きです。必ずテストを1個は書くことになるのでそれも良いです。この辺からテストをwatchさせておきます。

で、動いたやつをButton.stories.tsに書いていって、見た目の確認と、propsを変えての動作確認をします。で、yarn storybook したら、あとは基本この3ファイルを行ったり来たり。デザインも自分の場合はFigmaも挟んで、Variants弄りながら数値確認しながら実装。

仕上がったらhuskyとlint-stagedでformatとlintとtest全種とを最終チェックしてもらってpushする感じ。開発初期はGitHub Actionsナシでもいいかなーと思っています。

e2eはこの段階ではほぼ書かないで、componentを使って実際の画面を作る時に書くのですが、まだそっちのフローは曖昧。開発速度、とか言ってられるレベルじゃないのですが、兎も角、終始、解決すべき問題に集中できて心が穏やかでいいです。

この調子で書いていくときっとテストが膨大になるので、削ったり調整するタイミングを入れていかないといけないのかな、と思いますが今の処秒で終わるので、20秒ぐらいかかるようになったら考えます。タグの付け方を知りたい。たぶんそろそろテストの本を読んだ方が良いんだろうな…。

一年後ぐらいに読み返して、わーダメダメだったなーと思えますように…。


Photo by Aliko Sunawang on Unsplash

Photo by Aung Soe Min on Unsplash

Photo by Kelly Sikkema on Unsplash


この記事を書いた人