デザインシステムを考慮したコンポーネント設計について考える

## 忙しい人向けまとめ

React やデザインシステムに触れたばかりだと意識しづらいポイント

- `className`を自由に渡せる設計にすると、スタイルの統一性が崩れる

- 子コンポーネントを適切に分割しないと、管理が難しくなる

- 呼び出し側で必要な`props`を定義してから実装しないと使いづらい
 
デザインシステムの意図を考慮しながらコンポーネントを設計することが大切

## Before: 課題のあるコンポーネント
:::note info
コード例はReact(フレームワークはNext.jsですが今回は関係ないです), スタイリングはvanilla-extractです
:::

```Button.tsx
interface ButtonProps {
  label?: string;
  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
  disabled?: boolean;
  icon?: React.ReactNode;
  iconPosition?: 'before' | 'after';
  isLoading?: boolean; // isLoadingがデザインシステムにない
  className?: string;
  style?: React.CSSProperties;
}

const Button: React.FC<ButtonProps> = ({
  label,
  icon,
  iconPosition = 'before',
  onClick,
  disabled,
  isLoading,
  className,
  style,
}) => {
  return (
    <button onClick={onClick} className={className} style={style} disabled={disabled}>
      {isLoading ? 'Loading..' : (
        <>
          {icon && iconPosition === 'before' && <span>{icon}</span>}
          {label}
          {icon && iconPosition === 'after' && <span>{icon}</span>}
        </>
      )}
    </button>
  );
};
export default Button;
```

### 課題点

- `className`を自由に渡せるため、デザインの統一性が崩れる

- `props`の整理が甘く、呼び出し側で余計な考慮が必要になる

- `label`や `icon`の使い方が統一されておらず、意図が伝わりにくい
```呼び出し側.tsx
<Button
      label="送信" // ラベルをchildrenにするかpropsにするのかは制限による
      onClick={() => alert('送信しました')}
      icon={<SendIcon />}
      iconPosition="after"
      className={primaryButtonM} // ここの自由度が高すぎるのと直感的でない
 />
```

## After: シンプルで使いやすい設計

デザインシステムごとに適切なコンポーネントを分け、`props`の自由度を抑えることで統一感を確保すると使いやすい。

```PrimaryButton.tsx

interface Props = {
  children: React.ReactNode;
  size?: 's' | 'm' | 'l';
  variant?: 'green' | 'blue' | 'lightBlue' | 'purple' | 'gold' | 'red';
  disabled: boolean; // ここらへんはReact.ComponentPropsでまとめちゃっていいかもしれない
  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

export const PrimaryButton: React.FC<Props> = ({
  children,
  size = 'm',
  variant = 'green',
  disabled = false,
  onClick,
}) => {
  return (
    <button
      className={`${buttonBase} ${sizes[size]} ${
        disabled ? primaryDisabled[variant] : primaryVariants[variant]
      }`}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
};
```

デザインシステムごとにスタイルを統一

- `className`を自由に渡せないので、見た目のバラつきを防ぐ

- `children`を必須にして柔軟なラベル表示を可能に
```呼び出し側.tsx
<PrimaryButton size="s" variant="purple" onClick={() => alert('送信しました')}>送信</PrimaryButton>
```
:::note info
余談:props名について
- デザインシステムのカラートークンを渡して運用する意図があるのでvariantにしている
- これもデザインシステムの意図に合わせるとよりわかりやすくなる
```
  variant = 'green', 
```
:::

## 別例(アイコンとラベルはセットの場合)

```FormButton.tsx
export const FormButton: React.FC<Props> = ({
  children,
  icon,
  onClick, 
  disabled,
  ...props
}) => {
  return (
    <button className={formButton} disabled={disabled} type="button" onClick={onClick} {...props}>
      <span className={iconStyle}>{icon}</span>
      {children}
    </button>
  );
};
```

- **アイコンとラベルはセット**で必須

- `className`を固定し、デザインの統一感を維持

- 必要最低限の`props`だけを渡せばOK

### 使い方
``` 呼び出し側.tsx
<FormButton icon={<RefreshCw />} onClick={handleClick}>
  更新
</FormButton>
```

:::note info
もし**アイコンとテキストの組み合わせを完全に固定**したくなったら ...
デザイナーと相談し、共通言語としてvariantを定義し
``` 呼び出し側.tsx
<FormButton variant="refresh" onClick={handleClick}>
  更新
</FormButton>
```

のように variant を受け取る形にするというように修正する
:::

## まとめ

- 呼び出し側で余計な考慮が必要となる**自由なprops**に注意

- **デザインシステムに沿って設計する**ことで統一感と管理しやすさが向上

- **コンポーネントごとにルールを決める**と、実装のブレが少なくなる

誰でも**迷わず使えるコンポーネント**を目指すとこんな感じになるのかなと思って書きましたが、間違っているところもあると思います。

レビュー指摘していて、リファレンスに困った例でもあります
デザシス x 実装についてリファレンスにできそうな関連書籍などあれば紹介いただけると嬉しいです。