🙄
React + TypeScript + Inertia.js環境でのpropsの仕組みと、効果的なコンポーネント設計
React Props とコンポーネント設計ガイド
概要
このドキュメントでは、React + TypeScript + Inertia.js環境でのpropsの仕組みと、効果的なコンポーネント設計について解説します。
📋 目次
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.title → title
|
| 読みやすさ | 使用する変数が一目で分かる |
| 保守性 | 変更箇所が少ない |
| 型安全性 | 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