Reactにおけるコンポーネント間のデータの受け渡しは、Propsという仕組みを通じて行われます。TypeScriptを使用することで、Propsの型を明確に定義し、より安全で保守しやすいコードを書くことができます。本記事では、Propsの基本概念から実践的な使用方法まで、親子コンポーネントの関係を通じて詳しく学習します。
Propsとは何か
Propsの基本概念
Props(プロパティの略)は、親コンポーネントから子コンポーネントにデータを渡すためのReactの仕組みです。これにより、コンポーネントを再利用可能にし、様々な状況で同じコンポーネントを使い回すことができます。
Propsの重要な特徴:
- 読み取り専用: 子コンポーネントはPropsを変更できない
- 単方向データフロー: 親から子への一方向のデータ流れ
- 動的な値: 変数、関数、オブジェクトなど様々な値を渡せる
- 型安全性: TypeScriptで型を定義することで安全性を確保
HTMLの属性との類似性
PropsはHTMLの属性に似ています。例えば、HTMLの<img>
タグでは:
<img src="image.jpg" alt="説明文" width="300" />
Reactコンポーネントでも同様に:
<UserCard name="田中太郎" age={30} email="tanaka@example.com" />
このように、コンポーネントに様々な値を渡すことができます。
基本的なPropsの型定義
シンプルなPropsの例
まず、最もシンプルなPropsの例から始めましょう:
// src/components/Greeting.tsx
import React from 'react';
interface GreetingProps {
name: string;
}
const Greeting: React.FC<GreetingProps> = ({ name }) => {
return <h1>こんにちは、{name}さん!</h1>;
};
export default Greeting;
このコンポーネントを使用する親コンポーネント:
// src/App.tsx
import React from 'react';
import Greeting from './components/Greeting';
const App: React.FC = () => {
return (
<div>
<Greeting name="田中太郎" />
<Greeting name="佐藤花子" />
<Greeting name="鈴木一郎" />
</div>
);
};
export default App;
複数のPropsを持つコンポーネント
より実用的な例として、ユーザー情報を表示するコンポーネントを作成します:
// src/components/UserProfile.tsx
import React from 'react';
interface UserProfileProps {
name: string;
age: number;
email: string;
isActive: boolean;
}
const UserProfile: React.FC<UserProfileProps> = ({
name,
age,
email,
isActive
}) => {
return (
<div className="user-profile">
<h2>{name}</h2>
<p>年齢: {age}歳</p>
<p>メール: {email}</p>
<p>ステータス: {isActive ? 'アクティブ' : '非アクティブ'}</p>
</div>
);
};
export default UserProfile;
使用例:
const App: React.FC = () => {
return (
<div>
<UserProfile
name="田中太郎"
age={30}
email="tanaka@example.com"
isActive={true}
/>
</div>
);
};
オプショナルなPropsの定義
必須ではないPropsの扱い
すべてのPropsが必須である必要はありません。TypeScriptの?
演算子を使用してオプショナルなPropsを定義できます:
interface ButtonProps {
label: string; // 必須
onClick: () => void; // 必須
variant?: 'primary' | 'secondary'; // オプショナル
disabled?: boolean; // オプショナル
size?: 'small' | 'medium' | 'large'; // オプショナル
}
const Button: React.FC<ButtonProps> = ({
label,
onClick,
variant = 'primary', // デフォルト値を設定
disabled = false, // デフォルト値を設定
size = 'medium' // デフォルト値を設定
}) => {
return (
<button
className={`btn btn-${variant} btn-${size}`}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
};
デフォルト値の設定方法
デフォルト値は複数の方法で設定できます:
// 方法1: 分割代入でデフォルト値を設定
const Button: React.FC<ButtonProps> = ({
label,
onClick,
variant = 'primary',
disabled = false
}) => {
// コンポーネントの実装
};
// 方法2: defaultPropsを使用(非推奨)
Button.defaultProps = {
variant: 'primary',
disabled: false
};
// 方法3: コンポーネント内で条件演算子を使用
const Button: React.FC<ButtonProps> = (props) => {
const variant = props.variant || 'primary';
const disabled = props.disabled || false;
// コンポーネントの実装
};
推奨される方法は、分割代入でデフォルト値を設定する方法です。
複雑なオブジェクトをPropsとして渡す
ネストしたオブジェクトの型定義
実際のアプリケーションでは、複雑なデータ構造をPropsとして渡すことがあります:
// 型定義
interface Address {
street: string;
city: string;
postalCode: string;
country: string;
}
interface User {
id: number;
name: string;
email: string;
address: Address;
hobbies: string[];
}
interface UserCardProps {
user: User;
showAddress?: boolean;
}
// コンポーネントの実装
const UserCard: React.FC<UserCardProps> = ({ user, showAddress = false }) => {
return (
<div className="user-card">
<h2>{user.name}</h2>
<p>ID: {user.id}</p>
<p>メール: {user.email}</p>
{showAddress && (
<div className="address">
<h3>住所</h3>
<p>{user.address.street}</p>
<p>{user.address.city}, {user.address.postalCode}</p>
<p>{user.address.country}</p>
</div>
)}
<div className="hobbies">
<h3>趣味</h3>
<ul>
{user.hobbies.map((hobby, index) => (
<li key={index}>{hobby}</li>
))}
</ul>
</div>
</div>
);
};
使用例:
const App: React.FC = () => {
const userData: User = {
id: 1,
name: '田中太郎',
email: 'tanaka@example.com',
address: {
street: '東京都渋谷区1-1-1',
city: '渋谷区',
postalCode: '150-0001',
country: '日本'
},
hobbies: ['読書', 'プログラミング', '映画鑑賞']
};
return (
<div>
<UserCard user={userData} showAddress={true} />
</div>
);
};
関数をPropsとして渡す
イベントハンドラーの型定義
関数もPropsとして渡すことができます。特にイベントハンドラーはよく使用されるパターンです:
interface TodoItemProps {
id: number;
text: string;
completed: boolean;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
onEdit: (id: number, newText: string) => void;
}
const TodoItem: React.FC<TodoItemProps> = ({
id,
text,
completed,
onToggle,
onDelete,
onEdit
}) => {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(text);
const handleSave = () => {
onEdit(id, editText);
setIsEditing(false);
};
return (
<div className={`todo-item ${completed ? 'completed' : ''}`}>
{isEditing ? (
<div>
<input
value={editText}
onChange={(e) => setEditText(e.target.value)}
/>
<button onClick={handleSave}>保存</button>
<button onClick={() => setIsEditing(false)}>キャンセル</button>
</div>
) : (
<div>
<span onClick={() => onToggle(id)}>{text}</span>
<button onClick={() => setIsEditing(true)}>編集</button>
<button onClick={() => onDelete(id)}>削除</button>
</div>
)}
</div>
);
};
コールバック関数の実装
親コンポーネントでコールバック関数を実装:
const TodoList: React.FC = () => {
const [todos, setTodos] = useState<TodoItemProps[]>([
{ id: 1, text: '買い物', completed: false },
{ id: 2, text: '洗濯', completed: true }
]);
const handleToggle = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const handleDelete = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const handleEdit = (id: number, newText: string) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
));
};
return (
<div>
{todos.map(todo => (
<TodoItem
key={todo.id}
{...todo}
onToggle={handleToggle}
onDelete={handleDelete}
onEdit={handleEdit}
/>
))}
</div>
);
};
子要素(children)をPropsとして受け取る
React.ReactNodeの使用
特別なPropsとしてchildren
があります。これにより、コンポーネントの間に他の要素を挟むことができます:
interface CardProps {
title: string;
children: React.ReactNode;
className?: string;
}
const Card: React.FC<CardProps> = ({ title, children, className = '' }) => {
return (
<div className={`card ${className}`}>
<div className="card-header">
<h2>{title}</h2>
</div>
<div className="card-content">
{children}
</div>
</div>
);
};
使用例:
const App: React.FC = () => {
return (
<div>
<Card title="ユーザー情報">
<p>名前: 田中太郎</p>
<p>メール: tanaka@example.com</p>
<button>編集</button>
</Card>
<Card title="お知らせ" className="highlight">
<ul>
<li>新機能がリリースされました</li>
<li>メンテナンスのお知らせ</li>
</ul>
</Card>
</div>
);
};
特定の型のchildrenを受け取る
より具体的な型のchildrenを受け取りたい場合:
interface ButtonGroupProps {
children: React.ReactElement<ButtonProps> | React.ReactElement<ButtonProps>[];
orientation?: 'horizontal' | 'vertical';
}
const ButtonGroup: React.FC<ButtonGroupProps> = ({
children,
orientation = 'horizontal'
}) => {
return (
<div className={`button-group button-group-${orientation}`}>
{children}
</div>
);
};
Propsの検証とエラーハンドリング
TypeScriptの型チェックの活用
TypeScriptを使用することで、コンパイル時に多くのエラーを捕捉できます:
// 型エラーの例
<UserProfile
name="田中太郎"
age="30" // エラー: string型だがnumber型が期待される
email={123} // エラー: number型だがstring型が期待される
isActive="true" // エラー: string型だがboolean型が期待される
/>
ランタイムでの値の検証
開発時にはTypeScriptが型チェックを行いますが、実行時にも値を検証したい場合があります:
interface ValidatedInputProps {
value: string;
type: 'email' | 'phone' | 'url';
onValidationChange: (isValid: boolean) => void;
}
const ValidatedInput: React.FC<ValidatedInputProps> = ({
value,
type,
onValidationChange
}) => {
const validateValue = (val: string): boolean => {
switch (type) {
case 'email':
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val);
case 'phone':
return /^[\d-]+$/.test(val);
case 'url':
return /^https?:\/\/.+/.test(val);
default:
return true;
}
};
const isValid = validateValue(value);
React.useEffect(() => {
onValidationChange(isValid);
}, [isValid, onValidationChange]);
return (
<input
value={value}
className={isValid ? 'valid' : 'invalid'}
placeholder={`${type}を入力してください`}
/>
);
};
再利用可能なコンポーネントの設計
ジェネリクス(総称型)の活用
より柔軟なコンポーネントを作成するためにジェネリクスを使用できます:
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<div className="list">
{items.map((item, index) => (
<div key={keyExtractor(item)} className="list-item">
{renderItem(item, index)}
</div>
))}
</div>
);
}
// 使用例
interface User {
id: number;
name: string;
email: string;
}
const UserList: React.FC = () => {
const users: User[] = [
{ id: 1, name: '田中太郎', email: 'tanaka@example.com' },
{ id: 2, name: '佐藤花子', email: 'sato@example.com' }
];
return (
<List
items={users}
keyExtractor={(user) => user.id}
renderItem={(user) => (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
)}
/>
);
};
コンポーネントの合成パターン
複雑なUIを構築するために、小さなコンポーネントを組み合わせるパターン:
// 基本的なModalコンポーネント
interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, children }) => {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
);
};
// Modal専用のヘッダーコンポーネント
interface ModalHeaderProps {
title: string;
onClose: () => void;
}
const ModalHeader: React.FC<ModalHeaderProps> = ({ title, onClose }) => (
<div className="modal-header">
<h2>{title}</h2>
<button onClick={onClose} className="close-button">×</button>
</div>
);
// 使用例
const App: React.FC = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<button onClick={() => setIsModalOpen(true)}>
モーダルを開く
</button>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<ModalHeader
title="設定"
onClose={() => setIsModalOpen(false)}
/>
<div className="modal-body">
<p>設定内容をここに表示</p>
</div>
</Modal>
</div>
);
};
パフォーマンスの最適化
React.memoを使った最適化
Propsの変更時のみ再レンダリングするように最適化:
interface ExpensiveComponentProps {
data: ComplexData[];
onItemClick: (item: ComplexData) => void;
}
const ExpensiveComponent = React.memo<ExpensiveComponentProps>(({
data,
onItemClick
}) => {
console.log('ExpensiveComponent rendered'); // デバッグ用
return (
<div>
{data.map(item => (
<div key={item.id} onClick={() => onItemClick(item)}>
{item.name}
</div>
))}
</div>
);
});
// カスタム比較関数を使用した最適化
const OptimizedComponent = React.memo<ExpensiveComponentProps>(
({ data, onItemClick }) => {
// コンポーネントの実装
},
(prevProps, nextProps) => {
// カスタム比較ロジック
return (
prevProps.data.length === nextProps.data.length &&
prevProps.data.every((item, index) =>
item.id === nextProps.data[index].id
)
);
}
);
useCallbackとuseMemoの活用
コールバック関数の最適化:
const ParentComponent: React.FC = () => {
const [items, setItems] = useState<Item[]>([]);
const [filter, setFilter] = useState('');
// useCallbackでコールバック関数をメモ化
const handleItemClick = useCallback((item: Item) => {
console.log('Item clicked:', item);
}, []);
// useMemoで計算結果をメモ化
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="フィルター"
/>
<ItemList
items={filteredItems}
onItemClick={handleItemClick}
/>
</div>
);
};
よくあるエラーとその解決方法
型エラーの解決
問題: 必須Propsが渡されていない
// エラー: Property 'name' is missing in type
<Greeting />
解決: 必須Propsを渡すか、オプショナルにする
// 解決方法1: Propsを渡す
<Greeting name="田中太郎" />
// 解決方法2: オプショナルにする
interface GreetingProps {
name?: string;
}
問題: 関数の型が一致しない
// エラー: Types of parameters don't match
const handleClick = (event: MouseEvent) => { /* ... */ };
<button onClick={handleClick}>クリック</button>
解決: 正しいイベント型を使用
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
/* ... */
};
実際の開発では、React開発者ツールを使用してPropsの値を確認し、デバッグすることが重要です。
まとめ
Propsは、Reactにおけるコンポーネント間のデータ受け渡しの核となる仕組みです。TypeScriptと組み合わせることで、型安全性を保ちながら再利用可能で保守しやすいコンポーネントを作成できます。
重要なポイント:
- Propsの型定義により、コンパイル時にエラーを検出
- オプショナルPropsとデフォルト値の適切な使用
- 複雑なオブジェクトや関数もPropsとして渡せる
- childrenを活用したコンポーネント合成
- パフォーマンス最適化のためのメモ化
次のステップとして、コンポーネントの状態管理(State)について学習することで、より動的で相互作用のあるUIを構築できるようになります。継続的な実践を通じて、効果的なReact + TypeScript開発スキルを身につけていきましょう。