TailwindCSSでコンポーネントを作成するときに意識していること
概要
個人的に TailwindCSS を使ったコンポーネント作成をするときに意識していることをまとめます
ComponentProps
を使う
1. 多くの人が述べていますが、拡張性を高めるためにReact.ComponentProps
を使います。
import { ComponentProps, FC } from 'react'
type ButtonProps = {
loading: boolean
} & ComponentProps<'button'>
export const Button: FC<ButtonProps> = ({ loading, children, ...props }) => {
return <button {...props}>{loading ? children : 'Loading...'}</button>
}
このようにすると、ページで用いるときにデフォルトのボタンと同じような使用感で用いることができます。
tailwind-merge
を使う
2. tailwind-merge
はclassName
の結合をいい感じにやってくれるライブラリです。
以下は公式より引用します
What is it for
If you use Tailwind with a component-based UI renderer like React or Vue, you're probably familiar with the situation that you want to change some styles of a component, but only in one place.
// React components with JSX syntax used in this example
function MyGenericInput(props) {
const className = `border rounded px-2 py-1 ${props.className || ''}`;
return <input {...props} className={className} />;
}
function MySlightlyModifiedInput(props) {
return (
<MyGenericInput
{...props}
className='p-3' // ← Only want to change some padding
/>
);
}
When the MySlightlyModifiedInput is rendered, an input with the className border rounded px-2 py-1 p-3 gets created. But because of the way the CSS cascade works, the styles of the p-3 class are ignored. The order of the classes in the className string doesn't matter at all and the only way to apply the p-3 styles is to remove both px-2 and py-1.
This is where tailwind-merge comes in.
function MyGenericInput(props) {
// ↓ Now `props.className` can override conflicting classes
const className = twMerge('border rounded px-2 py-1', props.className);
return <input {...props} className={className} />;
}
tailwind-merge overrides conflicting classes and keeps everything else untouched. In the case of the MySlightlyModifiedInput, the input now only renders the classes border rounded p-3.
簡単にまとめると、
「px-2 py-1
ではなく、p-3
にするときはpx-2 py-1
を消さないとうまく繁栄されないよ!tw-merge
はそこらへんの処理をうまくやってくれるよ!」
ってことです。
このように元のclassName
をコンフリクトすることなくオーバーライドすることができます。
これを使ってベーススタイルを持ちつつ、拡張性の高いコンポーネントを作成できます。
import { ComponentProps, FC } from 'react'
+import { twMerge } from 'tailwind-merge'
type ButtonProps = {
loading: boolean
} & ComponentProps<'button'>
export const Button: FC<ButtonProps> = ({
loading,
children,
+ className,
...props
}) => {
+ const baseClass =
+ 'inline-block px-4 py-2 text-xs font-bold text-white bg-blue rounded-full'
+ const mergedClass = twMerge(baseClass, className)
return (
+ <button className={mergedClass} {...props}>
{loading ? children : 'Loading...'}
</button>
)
}
ref
を使うときはforwardRef
・componentPropsWithRef
3. react-hook-foom で以下のような書き方を見ると思います。
<input {...register('name')} />
これはそのまま扱うことはできません。register
の戻り値にref
が含まれているからです。
その場合はforwardRef
とcomponentPropsWithRef
を使ってコンポーネントを作成します。
import { ComponentPropsWithRef, forwardRef } from 'react'
import { twMerge } from 'tailwind-merge'
type InputProps = {
label: string
// labelとinputのclassNameを分ける
labelClassName?: string
inputClassName?: string
} & Omit<ComponentPropsWithRef<'input'>, 'className'>
+export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, labelClassName, inputClassName, ...props }, ref): JSX.Element => {
const baseLabelClass = 'text-xs font-bold text-gray-600'
const baseInputClass =
'w-full px-4 py-2 text-sm text-gray-600 border border-gray-300 rounded-md focus:border-blue focus:outline-none'
const mergedLabelClass = twMerge(baseLabelClass, labelClassName)
const mergedInputClass = twMerge(baseInputClass, inputClassName)
return (
<div className="flex flex-col">
<label className={mergedLabelClass}>{label}</label>
+ <input ref={ref} className={mergedInputClass} {...props} />
</div>
)
},
)
+Input.displayName = 'Input'
forwardRef<{HTMLの要素}, {Props}>
という感じでジェネリクスに Type を入れます。
また、forwardRef を使用した場合、eslint などの設定によっては「コンポーネントの名前がないよ!」と怒られるので、明示的にInput.displayName = 'Input'
と指定しています。
終わりに
このような使い回しの多いコンポーネントは型なども含め、きちんと定義した方が後々負債になりにくいです。
より良い開発体験を目指していきましょう!
Discussion
2の
tailwind-merge
は,classnames
でも代替できますか?代替はできません!
2で紹介したコードを用いて詳細に解説します
この場合、
input
のclassName
はとなります。一見意図したように動くように思いますが、
p-3
はpx-2
,py-1
があるため、反映されません。このような
className
にならないといけないのです。tw-merge
はこの処理を行っています。一方、
classnames
はclassName
の結合などを条件などに基づいて行うライブラリのため、この機能は提供されていませんそのため、代替ができません!
なるほど、競合ってやつですね?
今まで
classnames
をtailwindcss
と使っていたのですが、これからは
tw-merge
に切り替えようと思います!classnames 以外の選択しないかなぁと調べてたらここにたどり着きました。
既存クラスの打消しは、classnamesでもやろうと思えばできた気がする。
ただ、呼び先のコンポーネントも打ち消せるように作る必要があるから tw-merge のほうが良さげですね