🙄

React + TypeScript + Inertia.js環境でのpropsの仕組みと、効果的なコンポーネント設計

に公開

React Props とコンポーネント設計ガイド

概要

このドキュメントでは、React + TypeScript + Inertia.js環境でのpropsの仕組みと、効果的なコンポーネント設計について解説します。

📋 目次

  1. propsの基本概念
  2. 分割代入(Destructuring)
  3. スプレッド構文(Spread Syntax)
  4. ラッパーコンポーネント設計
  5. 実践例
  6. ベストプラクティス

propsの基本概念

props(properties)は、Reactコンポーネント間でデータを渡すためのメカニズムです。

// 基本的な使用例
<MyComponent title="Hello" count={42} />

TypeScriptでのprops型定義

interface MyComponentProps {
    title: string; // 必須のprop
    count: number; // 必須のprop
    description?: string; // オプショナルなprop(?マーク)
}

分割代入(Destructuring)

propsオブジェクトから特定の値を取り出す効率的な方法です。

🔰 分割代入の前提知識

まず、通常のオブジェクトアクセスから理解しましょう:

// 通常のオブジェクト
const user = {
    name: 'さあや',
    age: 15,
    hobby: 'AWS学習',
};

// 従来の方法でアクセス
const name = user.name; // 'さあや'
const age = user.age; // 15
const hobby = user.hobby; // 'AWS学習'

🎯 分割代入の基本

同じことを、より簡潔に書ける方法が分割代入です:

// 分割代入を使用
const { name, age, hobby } = user;

// 結果は同じ
console.log(name); // 'さあや'
console.log(age); // 15
console.log(hobby); // 'AWS学習'

📊 視覚的な理解

// これが...
const props = {
    title: 'AWSの中学校',
    count: 42,
    description: '楽しく学習!',
};

// こうなる
const { title, count, description } = props;
//      ↓      ↓          ↓
//   'AWSの中学校'  42    '楽しく学習!'

🔧 Reactでの実践的な使用

Step 1: 従来の方法

// 従来の方法(冗長)
function MyComponent(props: MyComponentProps) {
    return (
        <div>
            <h1>{props.title}</h1>
            <p>カウント: {props.count}</p>
            <p>{props.description}</p>
        </div>
    );
}

Step 2: 分割代入を使用

// 分割代入を使用(スッキリ!)
function MyComponent({ title, count, description }: MyComponentProps) {
    return (
        <div>
            <h1>{title}</h1>
            <p>カウント: {count}</p>
            <p>{description}</p>
        </div>
    );
}

🎭 オプショナルプロパティの分割代入

// オプショナルプロパティも分割代入可能
function UserCard({ name, age, email }: UserCardProps) {
    return (
        <div>
            <h2>{name}</h2>
            <p>年齢: {age}</p>
            {email && <p>メール: {email}</p>}  {/* emailがある場合のみ表示 */}
        </div>
    );
}

🎯 デフォルト値の設定

// デフォルト値を設定
function Button({ text = 'クリック', color = 'blue' }: ButtonProps) {
    return <button style={{ color }}>{text}</button>;
}

// 使用例
<Button />                    // 'クリック' + 'blue'
<Button text="送信" />        // '送信' + 'blue'
<Button color="red" />        // 'クリック' + 'red'

🔄 実際の例(app-layout.tsx)詳細解説

export default ({
    children,      // 子コンポーネントを分割代入で取得
    breadcrumbs,   // パンくずリストを分割代入で取得
    ...props       // 残りの全てのpropsを「...props」で取得
}: AppLayoutProps) => (
    // コンポーネントの実装
);

これを段階的に理解すると:

// Step 1: 通常の方法だと...
function AppLayout(allProps: AppLayoutProps) {
    const children = allProps.children;
    const breadcrumbs = allProps.breadcrumbs;
    const otherProps = { /* 残りのpropsを手動で抽出... */ };

    return (
        <AppLayoutTemplate breadcrumbs={breadcrumbs} {...otherProps}>
            {children}
        </AppLayoutTemplate>
    );
}

// Step 2: 分割代入を使うと...
function AppLayout({ children, breadcrumbs, ...props }: AppLayoutProps) {
    // 自動的に以下のように処理される:
    // - children: 子コンポーネント
    // - breadcrumbs: パンくずリスト
    // - props: 残りの全てのprops

    return (
        <AppLayoutTemplate breadcrumbs={breadcrumbs} {...props}>
            {children}
        </AppLayoutTemplate>
    );
}

:::note warn
よくある間違い
:::

1. 存在しないプロパティを分割代入

// ❌ 間違い: 存在しないプロパティ
const { name, age, salary } = user; // salaryが存在しない場合、undefinedになる

// ✅ 正しい: デフォルト値を設定
const { name, age, salary = 0 } = user;

2. 型定義との不整合

// ❌ 間違い: 型定義と異なる名前
function MyComponent({ title, count }: { name: string; age: number }) {
    // titleとcountは存在しない!
}

// ✅ 正しい: 型定義と一致
function MyComponent({ name, age }: { name: string; age: number }) {
    // nameとageは存在する
}

🎉 分割代入のメリット

メリット 説明
簡潔性 props.titletitle
読みやすさ 使用する変数が一目で分かる
保守性 変更箇所が少ない
型安全性 TypeScriptで型チェック

スプレッド構文(Spread Syntax)

残りのpropsを一括で処理する便利な機能です。

基本的なスプレッド構文

// propsの収集
const { title, ...restProps } = props;

// propsの展開
<ChildComponent {...restProps} />

実際の使用例

// app-layout.tsx での使用
<AppLayoutTemplate
    breadcrumbs={breadcrumbs}  // 特定のpropは明示的に渡す
    {...props}                 // 残りのpropsを全て展開して渡す
>
    {children}
</AppLayoutTemplate>

ラッパーコンポーネント設計

設計パターン

// ラッパーコンポーネント(app-layout.tsx)
export default ({ children, breadcrumbs, ...props }: AppLayoutProps) => (
    <AppLayoutTemplate breadcrumbs={breadcrumbs} {...props}>
        {children}
    </AppLayoutTemplate>
);

この設計の利点

利点 説明
分離 UIロジックと実装を分離
拡張性 新しいpropsを追加しても自動対応
保守性 変更箇所を局所化
再利用性 異なる実装に切り替え可能

実践例

1. コンポーネントの定義

// resources/js/layouts/app-layout.tsx
import AppLayoutTemplate from '@/layouts/app/app-sidebar-layout';
import { type BreadcrumbItem } from '@/types';
import { type ReactNode } from 'react';

interface AppLayoutProps {
    children: ReactNode;        // 子コンポーネント(必須)
    breadcrumbs?: BreadcrumbItem[];  // パンくずリスト(オプション)
}

export default ({
    children,      // 分割代入で取得
    breadcrumbs,   // 分割代入で取得
    ...props       // 残りのpropsを取得
}: AppLayoutProps) => (
    <AppLayoutTemplate
        breadcrumbs={breadcrumbs}  // 明示的に渡す
        {...props}                 // 残りを展開して渡す
    >
        {children}
    </AppLayoutTemplate>
);

2. コンポーネントの使用

// resources/js/pages/dashboard/introduction.tsx
import AppLayout from '@/layouts/app-layout';

const breadcrumbs: BreadcrumbItem[] = [
    { title: 'Dashboard', href: '/dashboard' },
    { title: 'はじめに', href: '/dashboard/introduction' },
];

export default () => (
    <AppLayout breadcrumbs={breadcrumbs} title="はじめに">
        <div>コンテンツ</div>
    </AppLayout>
);

データの流れ

ベストプラクティス

1. 型安全性の確保

// 必ずTypeScriptの型定義を使用
interface Props {
    title: string;
    count?: number; // オプショナルは?マーク
}

2. 適切な分割代入

// 使用するpropsのみを分割代入
const { title, description, ...rest } = props;

3. propsの命名規則

// 明確で分かりやすい名前を使用
interface UserCardProps {
    user: User;
    onEdit: () => void;
    showActions?: boolean;
}

4. デフォルト値の設定

// デフォルト値を設定
const { showActions = true, ...rest } = props;

注意点

:::note alert
パフォーマンス
:::

// 毎回新しいオブジェクトを作らない<Component {...{ title: 'test' }} />const props = { title: 'test' }; <Component {...props} />

:::note warn
型の整合性
:::

// propsの型が一致していることを確認
interface ParentProps extends ChildProps {
    additionalProp: string;
}

まとめ

  • 分割代入でpropsを効率的に取得
  • スプレッド構文で残りのpropsを一括処理
  • ラッパーコンポーネントで関心の分離を実現
  • TypeScriptで型安全性を確保

この設計により、保守性が高く、拡張しやすいReactコンポーネントを構築できます。

参考資料


このドキュメントは「AWSの中学校」プロジェクトの一部として作成されました。

Discussion