はじめに
いつもコンポーネントに切り出すのは同じような記述が2回以上ある場合に行っていました。
リファクタリングしてコードレビューを受ける中で、「buttonタグを使っていて、スタイルが同じで、色だけ変わるようなところは、Buttonコンポーネントにしてもいいのでは」というフィードバックをいただき、buttonタグはほぼすべて色違い、幅違いくらいだったので全buttonタグで使えるコンポーネントを作りました。
初めて出会った型
フィードバックいただいた内容や調べる中でナニソレな型にいくつか出会いました。
一晩かけてググっては読み、ググっては読み、時には頭を抱えながら、寝る前布団の中でAIに私の理解に間違いがないか確認しながら、最終的にレビューでLGTMいただける形に作り上げたので私の解釈をまとめていきます!
ComponentPropsWithRef
下記のように記述するとbuttonタグが持つすべての属性をpropsにすることができる型です。
ComponentPropsWithRef<"button">
今回はrefを許容したかったのでComponentPropsWithRefを使いましたが、許容したくない場合はComponentPropsWithoutRefを使います。
使う属性だけ列挙していかなくてもこれだけでまるっと指定できるので大変便利だと思いました!!
Omit
特定の属性だけを除去できる型です。
今回はスタイルを統一するためにコンポーネントをつくったので、classNameを渡されては困りますので、classNameを除去しました。
Omit<ComponentPropsWithRef<"button">, "className">
背景色はpropsでvariantを用意して、そちらでclassNameを変えられるようにしています。
レビューでは、「propsにclassNameは別で用意しておいて、propsで受け取ったclassNameに、ここで定義するclassNameを追加する処理にするのが良さそう」とのご指摘をいただきました。
今回はパターン4つでvariantで指定するため、Omitで対応することにしました。
より汎用性を上げて、スタイルも親コンポーネントで自由に操作できるようにしようと考えたらpropsにclassNameを用意しないと不具合生じることがあると教わりました。
本当にコードレビューありがたいです。
厳しく見てほしいとリクエストするくらいコードレビューでスキルアップしようとしています(笑)
forwardRef
親コンポーネントから子コンポーネントにref
を渡すための仕組みです。
refが渡せるとhookformも使えるからこうした方がいいとアドバイスいただきました。
正直refってなに状態でしたが結構初歩的な内容だったみたいで、調べたりAIに聞いたりして私がしっくりきたのが、refとは「DOM要素やクラスコンポーネントのインスタンスにアクセスするための手段」であり、forwardRefを使うことで関数コンポーネントでもrefを受け取れるようになるということでした。
つまり、refを渡せるようにしていなかったら(forwardRefを使わなかったら)子コンポーネントのインスタンスに直接アクセスできないので、例えばフォーカスの設定やアニメーションの制御、フォームバリデーション等を別途状態管理などを行って操作しないといけないということになります。
とにかく子コンポーネントが持っている値に直接アクセスできるから管理が楽になるんだなと解釈しました!!
最終的なコンポーネント
import { ReactNode, ComponentPropsWithRef, forwardRef } from "react";
type Variant =
| "outlined"
| "contained-blu"
| "contained-gry"
| "contained-blu500";
interface Props extends Omit<ComponentPropsWithRef<"button">, "className"> {
variant?: Variant;
children?: ReactNode;
}
export const Button = forwardRef<HTMLButtonElement, Props>(
({ variant, children, ...props }, ref) => {
const className = () => {
switch (variant) {
case "outlined":
return "border-solid border-2 border-slate-600";
case "contained-blu":
return "bg-custom-blue";
case "contained-gry":
return "bg-gray-300";
case "contained-blu500":
return "bg-blue-500 text-white";
default:
return "";
}
};
return (
<button
ref={ref}
{...props}
className={`w-full h-full rounded-full ${className()}`}
>
{children}
</button>
);
}
);
Button.displayName = "Button";
ボタン自体のサイズは親コンポーネントで指定します。
色の種類が増えた場合はvariantのパターン追加してswitchのcaseを増やしていこうと思います。
variant渡さずchildrenにImage渡したりするような使い方もしていますが、私の個人開発のプロジェクトではすべてこのButtonコンポーネントに置き換えができました!!
所感
TypeScript、難しいと感じるシーンも多くあるのですが、使いこなせたらめちゃくちゃ楽にしてくれる強い味方だなと改めて感じました。
今回buttonで作りましたが、他の要素でも応用効く内容だったので、汎用性の高いコンポーネントどんどん作っていきたいと思いました!!
コードレビューがありがた過ぎます!ちょっとずつレベル上げてくださっているので(多分)、これからもしがみついていきたいと思います!