Reactのボタンコンポーネントを1行ずつ解説する
この記事で何を解説するのか
実務でよく使われるボタンコンポーネントのコードを、1行ずつ分解して解説します。このコードには、Reactの重要な概念が詰まっています。
- 型とは何か
- propsの拡張とは何か
- デフォルト値の設定
- スプレッド構文
- 条件分岐
読み終わる頃には、このコードがなぜこう書かれているのか、理解できるようになるはずです。
前提知識: 「型」とは何か
コードを読む前に、「型」という概念を理解する必要があります。
型の基本
型とは、データの種類を指定するルールです。
let name: string = "田中"; // 文字列型
let age: number = 25; // 数値型
let isStudent: boolean = true; // 真偽値型
string: 文字列だけ入るnumber: 数値だけ入るboolean: trueかfalseだけ入る
なぜ型が必要なのか
型があると、間違いを防げるからです。
let age: number = 25;
age = "二十五才"; // エラー! 数値以外は入れられない
TypeScriptは、コードを書いている時点でエラーを教えてくれます。
型定義とは
型定義とは、どんなデータを受け取るかの設計図です。
type UserData = {
name: string;
age: number;
};
この設計図は「nameは文字列、ageは数値を受け取る」という約束です。
// OK
const user: UserData = {
name: "田中",
age: 25
};
// エラー! ageが文字列になっている
const user2: UserData = {
name: "佐藤",
age: "25才"
};
この「型定義」を理解すると、これから読むコードが理解できます。
コード全体
import { ButtonHTMLAttributes, ReactNode } from 'react'
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode
variant?: 'primary' | 'secondary' | 'danger'
isLoading?: boolean
}
export function Button({
children,
variant = 'primary',
isLoading = false,
className = '',
disabled,
...props
}: ButtonProps) {
const baseClasses = 'px-4 py-2 rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed'
const variantClasses = {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
danger: 'bg-red-500 text-white hover:bg-red-600',
}
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
disabled={disabled || isLoading}
{...props}
>
{isLoading ? '読み込み中...' : children}
</button>
)
}
1行目: import文
import { ButtonHTMLAttributes, ReactNode } from 'react'
何をしているか
Reactから2つの型定義をインポートしています。
ButtonHTMLAttributes<HTMLButtonElement>とは
HTMLの<button>タグが持つすべての属性の型定義です。
具体的には:
onClick: クリック時の処理を書く関数disabled: ボタンを無効化するか(true/false)type: ボタンのタイプ("button", "submit", "reset")className: CSSのクラス名(文字列)id,style,aria-*など、100以上の属性
つまり、普通のHTMLボタンに書けることすべてが含まれています。
ReactNodeとは
Reactで表示できるすべてのものを表す型です。
具体例:
- 文字列:
"クリック" - 数値:
123 - JSX要素:
<span>アイコン</span> - 配列:
[<Icon />, "送信"] - null, undefined(画面には表示されない)
要するに、ボタンの中に入れられるものすべてを表す型です。
なぜこの2つをインポートするのか
ButtonHTMLAttributes: 通常のボタンが持つ機能を全部使えるようにするReactNode: ボタンの中身(children)に何でも入れられるようにする
3-7行目: ButtonPropsとは何か
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode
variant?: 'primary' | 'secondary' | 'danger'
isLoading?: boolean
}
ButtonPropsの役割
ButtonPropsは、このボタンコンポーネントが受け取れるデータの設計図です。
まだピンと来ないと思うので、例で説明します。
設計図がない場合
// どんなデータを渡せばいいか分からない
<Button ???>
設計図がある場合
// ButtonPropsという設計図があるので、何を渡せばいいか分かる
<Button variant="primary" isLoading={false}>
送信
</Button>
ButtonPropsが「このボタンにはvariantとisLoadingが渡せます」と教えてくれます。
3行目: interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
interface ButtonProps
設計図の名前です。「ButtonPropsという名前の設計図を作る」という宣言です。
extends ButtonHTMLAttributes<HTMLButtonElement>
extendsは「継承する」という意味です。既にある設計図を引き継ぐことです。
図で表すと:
ButtonHTMLAttributes (既存の設計図)
├─ onClick
├─ disabled
├─ className
└─ その他100以上の属性
↓ extends (引き継ぐ)
ButtonProps (新しい設計図)
├─ onClick ← ButtonHTMLAttributesから
├─ disabled ← ButtonHTMLAttributesから
├─ className ← ButtonHTMLAttributesから
├─ children ← 新しく追加
├─ variant ← 新しく追加
└─ isLoading ← 新しく追加
つまり:
- 既存のボタンの機能(onClick, disabledなど)は全部使える
- さらに独自の機能(variant, isLoadingなど)も追加する
具体例:
// ButtonHTMLAttributesから使える機能
<Button onClick={() => alert('clicked')}>送信</Button>
<Button disabled={true}>送信</Button>
<Button className="mt-4">送信</Button>
// ButtonPropsで新しく追加した機能
<Button variant="primary">送信</Button>
<Button isLoading={true}>送信</Button>
// 全部組み合わせられる
<Button
variant="danger"
isLoading={false}
onClick={() => alert('削除します')}
className="w-full"
>
削除
</Button>
4行目: children: ReactNode
ボタンの中身を受け取る型定義です。
<Button>送信</Button>
// children = "送信" (文字列)
<Button><Icon />送信</Button>
// children = [<Icon />, "送信"] (配列)
ReactNode型なので、文字列でもJSXでも何でも入ります。
5行目: variant?: 'primary' | 'secondary' | 'danger'
variantの意味
variantは「変化形」「バリエーション」という意味です。
このボタンコンポーネントでは、ボタンの見た目のバリエーションを指定します。
primary: メインのボタン(青色)secondary: サブのボタン(グレー)danger: 危険な操作のボタン(赤色)
?の意味
省略可能という意味です。variantを渡さなくてもエラーになりません。
<Button>送信</Button> // variantなし → OK
<Button variant="primary">送信</Button> // variantあり → OK
'primary' | 'secondary' | 'danger'の意味
この3つの文字列だけを受け付けます。
<Button variant="primary"> // OK
<Button variant="secondary"> // OK
<Button variant="danger"> // OK
<Button variant="warning"> // エラー! 'warning'は設計図にない
<Button variant="成功"> // エラー! 日本語は設計図にない
なぜこう定義するのか
ボタンの見た目を3種類に限定するためです。
もし何でも受け付けると:
variant?: string // 何でもOK
<Button variant="あいうえお"> // 変なデータが入る
こうなると、対応する色が定義されていないのでエラーになります。
だから、使える値を限定する必要があります。
6行目: isLoading?: boolean
ローディング中かどうかのフラグです。
true: ローディング中false: 通常
?があるので省略可能です。
<Button isLoading={true}> // ローディング中
<Button isLoading={false}> // 通常
<Button> // 省略(デフォルトでfalseになる)
ButtonPropsとfunction Buttonの違い
ここまで読んで、こう思ったはずです。
「ButtonPropsとfunction Buttonは何が違うの?」
ButtonPropsの役割
設計図です。「どんなデータを受け取れるか」を定義します。
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode
variant?: 'primary' | 'secondary' | 'danger'
isLoading?: boolean
}
これは「受け取れるデータの種類」を書いただけです。実際の処理は何も書いていません。
function Buttonの役割
実際の処理を書く場所です。受け取ったデータを使って、ボタンを作ります。
export function Button({ children, variant, ... }: ButtonProps) {
// 受け取ったデータを使って、実際にボタンを作る処理
return <button>...</button>
}
図で表すと
ButtonProps (設計図)
↓
「variantは'primary' | 'secondary' | 'danger'だけ受け取れます」「isLoadingはboolean型です」
↓ 設計図を使って
function Button (実際の処理)
↓
「受け取ったvariantに応じて、色を変えます」「isLoadingがtrueなら、ボタンを無効化します」
例え話
ButtonProps = 料理のレシピ
- 材料: 鶏肉、玉ねぎ、カレールー
- 分量: 鶏肉300g、玉ねぎ1個
function Button = 実際の調理
- 材料を切る
- 炒める
- 煮込む
- 完成
レシピがあっても、調理しないと料理はできません。同様に、ButtonPropsがあっても、function Buttonがないとボタンは作れません。
9-16行目: 関数定義とpropsの受け取り
export function Button({
children,
variant = 'primary',
isLoading = false,
className = '',
disabled,
...props
}: ButtonProps) {
9行目: export function Button(
Buttonという名前の関数(コンポーネント)を作り、外部から使えるようにします。
10-16行目: データの受け取り方
{
children,
variant = 'primary',
isLoading = false,
className = '',
disabled,
...props
}: ButtonProps
末尾の: ButtonProps
「この関数はButtonPropsという設計図に従ってデータを受け取る」という宣言です。
つまり、ButtonPropsで定義した型以外は受け取れません。
// OK: ButtonPropsに定義されている
<Button variant="primary">送信</Button>
// エラー: ButtonPropsに定義されていない
<Button color="red">送信</Button>
{ children, variant, ... }の意味(分割代入)
propsオブジェクトから、必要な値を取り出す書き方です。
分割代入を使わない場合:
function Button(props: ButtonProps) {
const children = props.children
const variant = props.variant
const isLoading = props.isLoading
// 毎回props.○○と書く必要がある
}
分割代入を使う場合:
function Button({ children, variant, isLoading }: ButtonProps) {
// そのまま使える
console.log(children)
console.log(variant)
}
デフォルト値の設定
variant = 'primary'は、variantが渡されなかった時の初期値です。
<Button>送信</Button>
// variantを渡していない
// → 自動的に variant = 'primary' になる
<Button variant="danger">削除</Button>
// variantを渡している
// → variant = 'danger' が使われる
同様に:
isLoading = false: 省略時はfalseclassName = '': 省略時は空文字
disabledにデフォルト値がない理由
disabledは元々のHTML仕様で省略可能だからです。
<!-- disabled属性がない = 有効 -->
<button>クリック</button>
<!-- disabled属性がある = 無効 -->
<button disabled>クリック</button>
だから、デフォルト値を設定する必要がありません。
...propsの意味(スプレッド構文)
残りのpropsをすべて受け取るという意味です。
具体例:
<Button
variant="primary"
onClick={() => alert('click')}
id="submit"
aria-label="送信ボタン"
>
送信
</Button>
この場合、データは以下のように分かれます:
// 個別に受け取るもの
variant = "primary"
// ...propsに入るもの
props = {
onClick: () => alert('click'),
id: "submit",
"aria-label": "送信ボタン"
}
なぜこうするのか?
すべてのpropsを個別に書くと大変だからです。
// これは大変
function Button({
children,
variant,
isLoading,
className,
disabled,
onClick,
onMouseEnter,
onMouseLeave,
id,
style,
// ... 100個以上書く必要がある
}: ButtonProps) {
...propsを使えば、残りは一括で受け取れます。
17行目: 基本スタイルの定義
const baseClasses = 'px-4 py-2 rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed'
すべてのボタンに共通するCSSクラス(見た目の設定)です。
px-4 py-2: 内側の余白(左右4、上下2)rounded-lg: 角を丸くするfont-medium: 文字の太さを中間にtransition-colors: 色が変わる時にアニメーションdisabled:opacity-50: 無効化時に半透明にするdisabled:cursor-not-allowed: 無効化時にカーソルを禁止マークに
これはvariantに関係なく、すべてのボタンに適用されます。
19-23行目: バリエーション別スタイル
const variantClasses = {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
danger: 'bg-red-500 text-white hover:bg-red-600',
}
これは何をしているのか
渡されたvariantによって、ボタンの色を変える設定です。
primary: 青いボタンsecondary: グレーのボタンdanger: 赤いボタン
オブジェクト形式で定義する理由
variantClasses[variant]
で、動的に取り出せるからです。
動作例:
variant = 'primary'
variantClasses[variant]
// → 'bg-blue-500 text-white hover:bg-blue-600'
variant = 'danger'
variantClasses[variant]
// → 'bg-red-500 text-white hover:bg-red-600'
各クラスの意味
primary(青いボタン):
bg-blue-500: 背景色を青にtext-white: 文字色を白にhover:bg-blue-600: マウスを乗せたら濃い青に
secondary(グレーのボタン):
bg-gray-200: 背景色を薄いグレーにtext-gray-800: 文字色を濃いグレーにhover:bg-gray-300: マウスを乗せたら少し濃いグレーに
danger(赤いボタン):
bg-red-500: 背景色を赤にtext-white: 文字色を白にhover:bg-red-600: マウスを乗せたら濃い赤に
まとめると
このコードは、propsで渡されたvariantの値に応じて、ボタンの見た目を変えているということです。
<Button variant="primary">送信</Button> // 青いボタン
<Button variant="secondary">キャンセル</Button> // グレーのボタン
<Button variant="danger">削除</Button> // 赤いボタン
25-33行目: JSXの返却
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
disabled={disabled || isLoading}
{...props}
>
{isLoading ? '読み込み中...' : children}
</button>
)
27行目: classNameの結合
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
テンプレートリテラル(バッククォート)
`文字列 ${変数} 文字列`の形式です。変数を文字列の中に埋め込めます。
展開例:
variant = 'primary'
className = 'mt-4'
// 結果
"px-4 py-2 rounded-lg ... bg-blue-500 text-white hover:bg-blue-600 mt-4"
3つのクラスが結合されます:
baseClasses: 共通スタイルvariantClasses[variant]: バリエーション別スタイルclassName: 外部から渡されたカスタムスタイル
28行目: disabledの条件
disabled={disabled || isLoading}
||の意味
「または」という意味です。
disabledがtrue またはisLoadingがtrue → ボタンを無効化- 両方false → ボタンは有効
具体例:
disabled = true, isLoading = false
→ true || false = true (無効化)
disabled = false, isLoading = true
→ false || true = true (無効化)
disabled = false, isLoading = false
→ false || false = false (有効)
disabled = true, isLoading = true
→ true || true = true (無効化)
つまり、どちらか一方でもtrueなら無効化するという意味です。
29行目: propsの展開
{...props}
残りのpropsをすべてボタンに渡します。
展開例:
props = {
onClick: () => alert('click'),
id: "submit",
type: "button"
}
// 展開後
<button
onClick={() => alert('click')}
id="submit"
type="button"
>
31行目: 条件分岐
{isLoading ? '読み込み中...' : children}
三項演算子
条件 ? 真の時 : 偽の時の形式です。
isLoadingがtrue → 「読み込み中...」と表示isLoadingがfalse →childrenを表示
具体例:
// ローディング中
<Button isLoading={true}>送信</Button>
// 表示: 「読み込み中...」
// 通常時
<Button isLoading={false}>送信</Button>
// 表示: 「送信」
全体の流れ
1. ボタンを呼び出す
<Button variant="danger" isLoading={false} onClick={() => {}}>
削除
</Button>
2. propsが渡される
children = "削除"
variant = "danger"
isLoading = false
props = { onClick: () => {} }
3. スタイルが組み立てられる
baseClasses = "px-4 py-2 ..."
variantClasses[variant] = "bg-red-500 text-white hover:bg-red-600"
className = ""
// 結合
className = "px-4 py-2 ... bg-red-500 text-white hover:bg-red-600"
4. disabledが判定される
disabled = false || false = false
// ボタンは有効
5. 表示内容が決まる
isLoading = false
// childrenを表示 → "削除"
6. 最終的なHTML
<button
class="px-4 py-2 rounded-lg ... bg-red-500 text-white hover:bg-red-600"
onClick={() => {}}
>
削除
</button>
このコンポーネントの使い方
基本的な使い方
<Button>送信</Button>
結果:
- 青いボタン(variant="primary"がデフォルト)
- テキストは「送信」
- クリック可能
バリエーションを変える
<Button variant="secondary">キャンセル</Button>
<Button variant="danger">削除</Button>
ローディング状態
<Button isLoading={true}>送信中</Button>
結果:
- ボタンは無効化される
- 表示は「読み込み中...」になる
- 元の「送信中」というテキストは表示されない
カスタムスタイルを追加
<Button className="w-full mt-4">送信</Button>
結果:
- 基本スタイル + バリエーションスタイル +
w-full mt-4 - 幅100%、上部に余白
HTML属性を渡す
<Button
onClick={() => alert('clicked')}
type="submit"
id="submit-btn"
>
送信
</Button>
すべて{...props}によってボタンに渡されます。
よくある疑問
疑問1: なぜextendsを使うのか
ButtonHTMLAttributesには100以上の属性があります。これを手動で全部書くのは不可能です。
extendsを使えば、自動的にすべて使えるようになります。
疑問2: ...propsは何のためか
予想できないpropsを受け取るためです。
<Button data-testid="submit-button">
data-testidは事前に型定義していませんが、...propsで受け取れます。
疑問3: なぜvariantにデフォルト値が必要か
省略時の動作を明確にするためです。
デフォルト値がないと:
variant: 'primary' | 'secondary' | 'danger' | undefined
undefinedの時の処理が必要になります。
デフォルト値があれば:
variant = 'primary' // 必ず値が入っている
まとめ
このボタンコンポーネントは、以下の技術を組み合わせています。
- 型の継承:
extends ButtonHTMLAttributesで既存の型を引き継ぐ - デフォルト値:
variant = 'primary'で省略時の値を設定 - スプレッド構文:
{...props}で残りのpropsを一括受け取り - 条件分岐:
isLoading ? '読み込み中...' : childrenで表示を切り替え - 動的スタイル:
variantClasses[variant]でpropsに応じてスタイルを変更
これらを理解すれば、実務レベルのコンポーネントが書けるようになります。
次にコンポーネントを作る時は、このパターンを思い出してください。