はじめに

前回の記事では、Reactの関数コンポーネントの作り方を解説しました。本記事では、コンポーネント間でデータを受け渡すための仕組み「Props」について詳しく解説します。

Propsは「Properties(プロパティ)」の略で、親コンポーネントから子コンポーネントへデータを渡すための仕組みです。Propsを正しく理解することで、再利用可能で柔軟なコンポーネントを設計できるようになります。

本記事を読むことで、以下のことができるようになります。

  • Propsの基本的な使い方の習得
  • TypeScriptを使ったPropsの型定義
  • childrenを使った柔軟なコンポーネント設計
  • デフォルト値の設定方法
  • Propsのイミュータビリティの理解

実行環境・前提条件

必要な環境

  • Node.js 20.x以上
  • Viteで作成したReactプロジェクト(TypeScript推奨)
  • VS Code(推奨)

前提知識

  • 関数コンポーネントの基本
  • JSXの基本構文
  • JavaScriptのオブジェクトと分割代入

Propsとは何か

Propsは、コンポーネントに渡されるデータをまとめたオブジェクトです。HTML要素の属性と同じような感覚で、コンポーネントに情報を渡すことができます。

Propsの基本的な使い方

1
2
3
4
5
6
7
8
9
// 親コンポーネント
function App() {
  return <Greeting name="太郎" />;
}

// 子コンポーネント
function Greeting(props) {
  return <h1>こんにちは{props.name}さん</h1>;
}

name="太郎"という形で渡されたデータが、子コンポーネントのpropsオブジェクトに含まれます。

期待される結果

1
<h1>こんにちは、太郎さん!</h1>

分割代入によるPropsの受け取り

実際の開発では、分割代入を使ってPropsを受け取ることが一般的です。

1
2
3
4
5
6
7
8
9
// props.name, props.age のように書く代わりに
function Greeting(props) {
  return <h1>こんにちは{props.name}さん{props.age})!</h1>;
}

// 分割代入で直接変数として受け取る
function Greeting({ name, age }) {
  return <h1>こんにちは{name}さん{age})!</h1>;
}

分割代入を使うと、どのPropsを受け取るかが一目でわかり、コードも簡潔になります。

さまざまな型のPropsを渡す

Propsには文字列だけでなく、さまざまな型のデータを渡すことができます。

文字列

1
<UserCard name="山田太郎" />

文字列はダブルクォートで囲みます。

数値

1
<Counter initialCount={10} />

数値は波括弧で囲んで渡します。

真偽値(Boolean)

1
2
3
4
5
6
7
8
// true を渡す場合
<Button disabled={true} />

// 属性名だけで true と解釈される
<Button disabled />

// false を渡す場合
<Button disabled={false} />

配列

1
<TagList tags={['React', 'JavaScript', 'TypeScript']} />

オブジェクト

1
2
3
4
5
6
7
<UserProfile 
  user={{ 
    name: '田中花子', 
    email: 'hanako@example.com',
    age: 28 
  }} 
/>

波括弧が二重になることに注意してください。外側の波括弧はJSXの式埋め込み、内側の波括弧はオブジェクトリテラルです。

関数

1
<Button onClick={() => alert('クリックされました')} />

イベントハンドラなどの関数もPropsとして渡せます。

複数のPropsを渡す実例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function App() {
  const handleClick = () => {
    console.log('ボタンがクリックされました');
  };
  
  return (
    <ProductCard
      name="ワイヤレスイヤホン"
      price={15800}
      inStock={true}
      tags={['電子機器', 'オーディオ', '新着']}
      rating={{ score: 4.5, count: 128 }}
      onAddToCart={handleClick}
    />
  );
}

function ProductCard({ name, price, inStock, tags, rating, onAddToCart }) {
  return (
    <div className="product-card">
      <h2>{name}</h2>
      <p className="price">{price.toLocaleString()}</p>
      <p className="stock">{inStock ? '在庫あり' : '在庫なし'}</p>
      <div className="tags">
        {tags.map((tag, index) => (
          <span key={index} className="tag">{tag}</span>
        ))}
      </div>
      <p className="rating">
        評価: {rating.score} ({rating.count})
      </p>
      <button onClick={onAddToCart} disabled={!inStock}>
        カートに追加
      </button>
    </div>
  );
}

TypeScriptでのProps型定義

TypeScriptを使用すると、Propsに型を定義して型安全なコードを書けます。

type を使った型定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type ButtonProps = {
  label: string;
  variant: 'primary' | 'secondary' | 'danger';
  disabled?: boolean;  // ? はオプショナル(省略可能)
  onClick: () => void;
};

function Button({ label, variant, disabled = false, onClick }: ButtonProps) {
  return (
    <button 
      className={`btn btn-${variant}`}
      disabled={disabled}
      onClick={onClick}
    >
      {label}
    </button>
  );
}

interface を使った型定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
interface CardProps {
  title: string;
  description: string;
  imageUrl?: string;
}

function Card({ title, description, imageUrl }: CardProps) {
  return (
    <div className="card">
      {imageUrl && <img src={imageUrl} alt={title} />}
      <h2>{title}</h2>
      <p>{description}</p>
    </div>
  );
}

type と interface の使い分け

どちらを使っても機能的には大きな差はありませんが、以下のガイドラインがあります。

  • type:ユニオン型やタプル型を使う場合に適している
  • interface:拡張(extends)を使う場合に適している

プロジェクトで統一されていれば、どちらを使っても問題ありません。

型定義のメリット

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 型定義があると、誤った使い方をコンパイル時に検出できる

// エラー: 'primary' | 'secondary' | 'danger' 以外の値
<Button label="送信" variant="info" onClick={handleClick} />

// エラー: label が欠けている
<Button variant="primary" onClick={handleClick} />

// エラー: onClick の型が違う
<Button label="送信" variant="primary" onClick="handleClick" />

children Props

childrenは特別なPropsで、コンポーネントの開始タグと終了タグの間に配置された要素を受け取ります。

childrenの基本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

// 使用例
function App() {
  return (
    <Card>
      <h2>カードのタイトル</h2>
      <p>カードの本文がここに入ります</p>
    </Card>
  );
}

期待される結果

1
2
3
4
<div class="card">
  <h2>カードのタイトル</h2>
  <p>カードの本文がここに入ります。</p>
</div>

childrenを使った柔軟なレイアウト

childrenを使うと、汎用的なレイアウトコンポーネントを作成できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function PageLayout({ children }) {
  return (
    <div className="page-layout">
      <Header />
      <main className="main-content">
        {children}
      </main>
      <Footer />
    </div>
  );
}

// 使用例
function HomePage() {
  return (
    <PageLayout>
      <h1>ホームページ</h1>
      <p>ようこそ</p>
    </PageLayout>
  );
}

function AboutPage() {
  return (
    <PageLayout>
      <h1>About</h1>
      <p>私たちについて</p>
    </PageLayout>
  );
}

複数の「スロット」を持つコンポーネント

children以外のPropsを使って、複数の挿入ポイントを持つコンポーネントを作成できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function Modal({ title, children, footer }) {
  return (
    <div className="modal-overlay">
      <div className="modal">
        <div className="modal-header">
          <h2>{title}</h2>
        </div>
        <div className="modal-body">
          {children}
        </div>
        {footer && (
          <div className="modal-footer">
            {footer}
          </div>
        )}
      </div>
    </div>
  );
}

// 使用例
function App() {
  return (
    <Modal 
      title="確認"
      footer={
        <>
          <button>キャンセル</button>
          <button>OK</button>
        </>
      }
    >
      <p>本当に削除しますか</p>
    </Modal>
  );
}

TypeScriptでのchildren型定義

1
2
3
4
5
6
7
8
9
import { ReactNode } from 'react';

type CardProps = {
  children: ReactNode;
};

function Card({ children }: CardProps) {
  return <div className="card">{children}</div>;
}

ReactNode型は、JSX要素、文字列、数値、配列、null、undefinedなど、Reactがレンダリングできるすべての型を許容します。

デフォルト値の設定

Propsにデフォルト値を設定することで、省略可能なPropsを定義できます。

分割代入でのデフォルト値

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Button({ 
  label, 
  variant = 'primary', 
  size = 'medium',
  disabled = false 
}) {
  return (
    <button 
      className={`btn btn-${variant} btn-${size}`}
      disabled={disabled}
    >
      {label}
    </button>
  );
}

// 使用例
<Button label="送信" />
// variant='primary', size='medium', disabled=false がデフォルトで適用される

<Button label="削除" variant="danger" size="small" />
// variant='danger', size='small' が上書きされる

TypeScriptでのデフォルト値

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type AlertProps = {
  message: string;
  type?: 'info' | 'warning' | 'error';  // オプショナル
  dismissible?: boolean;
};

function Alert({ 
  message, 
  type = 'info', 
  dismissible = true 
}: AlertProps) {
  return (
    <div className={`alert alert-${type}`}>
      <p>{message}</p>
      {dismissible && <button>×</button>}
    </div>
  );
}

オブジェクトのデフォルト値

オブジェクト型のPropsにデフォルト値を設定する場合は注意が必要です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 注意:オブジェクト全体のデフォルト値
function UserCard({ user = { name: 'ゲスト', role: 'visitor' } }) {
  return (
    <div>
      <p>{user.name}</p>
      <p>{user.role}</p>
    </div>
  );
}

// 個別のプロパティにデフォルト値を設定する方が柔軟
function UserCard({ user }) {
  const { name = 'ゲスト', role = 'visitor' } = user || {};
  
  return (
    <div>
      <p>{name}</p>
      <p>{role}</p>
    </div>
  );
}

Propsのイミュータビリティ

Reactにおいて、Propsは読み取り専用(イミュータブル)です。子コンポーネント内でPropsを変更してはいけません。

なぜPropsは変更してはいけないのか

1
2
3
4
5
// 絶対にやってはいけない例
function BadComponent({ user }) {
  user.name = '変更された名前';  // Propsの変更は禁止
  return <p>{user.name}</p>;
}

Propsを変更すると以下の問題が発生します。

  • 予測不能な動作:親コンポーネントのデータが意図せず変更される
  • デバッグの困難さ:データの変更元を追跡しにくくなる
  • 再レンダリングの問題:Reactの最適化が正しく動作しなくなる

正しいデータ変更のパターン

データを変更する必要がある場合は、親コンポーネントから変更関数を渡します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function App() {
  const [user, setUser] = useState({ name: '太郎', age: 25 });
  
  const handleNameChange = (newName) => {
    setUser({ ...user, name: newName });
  };
  
  return <UserEditor user={user} onNameChange={handleNameChange} />;
}

function UserEditor({ user, onNameChange }) {
  return (
    <div>
      <p>現在の名前: {user.name}</p>
      <input 
        type="text"
        value={user.name}
        onChange={(e) => onNameChange(e.target.value)}
      />
    </div>
  );
}

このパターンにより、データの変更は常に親コンポーネントで行われ、データフローが明確になります。

スプレッド構文によるPropsの転送

スプレッド構文を使うと、すべてのPropsを一度に子コンポーネントに転送できます。

基本的な転送

1
2
3
4
5
6
7
8
function EnhancedButton(props) {
  return <button className="enhanced-btn" {...props} />;
}

// 使用例
<EnhancedButton onClick={handleClick} disabled={false}>
  クリック
</EnhancedButton>

一部のPropsを抽出して残りを転送

1
2
3
4
5
6
7
8
function Card({ highlighted, ...rest }) {
  return (
    <div 
      className={highlighted ? 'card highlighted' : 'card'} 
      {...rest}
    />
  );
}

highlightedCardコンポーネントで使用し、それ以外のPropsは<div>に転送されます。

注意点

スプレッド構文は便利ですが、以下の点に注意が必要です。

  • どのPropsが渡されるか不明確になりやすい
  • 意図しないPropsがDOMに渡される可能性がある
  • TypeScriptでの型推論が複雑になる

明示的にPropsを列挙する方が、コードの意図が明確になることが多いです。

Props設計のベストプラクティス

効果的なコンポーネント設計のためのガイドラインを紹介します。

1. 必要最小限のPropsを渡す

1
2
3
4
5
// 良くない例:オブジェクト全体を渡す
<UserAvatar user={user} />

// 良い例:必要なプロパティだけを渡す
<UserAvatar name={user.name} imageUrl={user.avatar} />

必要なデータだけを渡すことで、コンポーネントの依存関係が明確になります。

2. 一貫した命名規則

1
2
3
4
5
6
7
8
// イベントハンドラは on + 動詞 の形式
<Button onClick={handleClick} />
<Form onSubmit={handleSubmit} />

// 真偽値は is/has/can で始める
<Modal isOpen={true} />
<Alert hasCloseButton={true} />
<Button canSubmit={isValid} />

3. 必須とオプショナルの明確化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type ButtonProps = {
  // 必須Props
  label: string;
  onClick: () => void;
  
  // オプショナルProps(デフォルト値を持つ)
  variant?: 'primary' | 'secondary';
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
};

4. コンポーネントの責任を明確に

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 良くない例:1つのコンポーネントに多くの責任
<DataTable
  data={users}
  fetchData={fetchUsers}
  sortColumn={sortColumn}
  onSort={handleSort}
  filterValue={filter}
  onFilter={handleFilter}
  pageSize={10}
  currentPage={page}
  onPageChange={handlePageChange}
/>

// 良い例:責任を分割
<DataFetcher onDataLoad={setUsers}>
  <SortableTable data={users} onSort={handleSort}>
    <FilterInput value={filter} onChange={handleFilter} />
    <Pagination 
      pageSize={10} 
      currentPage={page} 
      onPageChange={handlePageChange}
    />
  </SortableTable>
</DataFetcher>

実践例:再利用可能なフォームコンポーネント

これまでの知識を活用した実践的な例を紹介します。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import { ReactNode } from 'react';

// 入力フィールドの型定義
type InputFieldProps = {
  label: string;
  name: string;
  type?: 'text' | 'email' | 'password' | 'number';
  value: string;
  onChange: (value: string) => void;
  error?: string;
  placeholder?: string;
  required?: boolean;
};

function InputField({
  label,
  name,
  type = 'text',
  value,
  onChange,
  error,
  placeholder,
  required = false
}: InputFieldProps) {
  return (
    <div className="form-field">
      <label htmlFor={name}>
        {label}
        {required && <span className="required">*</span>}
      </label>
      <input
        id={name}
        name={name}
        type={type}
        value={value}
        onChange={(e) => onChange(e.target.value)}
        placeholder={placeholder}
        className={error ? 'input-error' : ''}
      />
      {error && <p className="error-message">{error}</p>}
    </div>
  );
}

// フォームコンテナの型定義
type FormContainerProps = {
  title: string;
  children: ReactNode;
  onSubmit: () => void;
  submitLabel?: string;
  isSubmitting?: boolean;
};

function FormContainer({
  title,
  children,
  onSubmit,
  submitLabel = '送信',
  isSubmitting = false
}: FormContainerProps) {
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    onSubmit();
  };

  return (
    <form className="form-container" onSubmit={handleSubmit}>
      <h2>{title}</h2>
      <div className="form-fields">{children}</div>
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '送信中...' : submitLabel}
      </button>
    </form>
  );
}

// 使用例
function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async () => {
    setIsSubmitting(true);
    // 送信処理
    setIsSubmitting(false);
  };

  return (
    <FormContainer
      title="お問い合わせ"
      onSubmit={handleSubmit}
      isSubmitting={isSubmitting}
    >
      <InputField
        label="お名前"
        name="name"
        value={formData.name}
        onChange={(value) => setFormData({ ...formData, name: value })}
        error={errors.name}
        required
      />
      <InputField
        label="メールアドレス"
        name="email"
        type="email"
        value={formData.email}
        onChange={(value) => setFormData({ ...formData, email: value })}
        error={errors.email}
        required
      />
    </FormContainer>
  );
}

まとめ

本記事では、ReactのPropsについて詳しく解説しました。

  • Propsの基本:親から子へデータを渡す仕組み、分割代入での受け取り
  • さまざまな型:文字列、数値、真偽値、配列、オブジェクト、関数をPropsとして渡せる
  • TypeScriptでの型定義:type/interfaceを使った型安全なProps定義
  • children:開始タグと終了タグの間の要素を受け取る特別なProps
  • デフォルト値:分割代入でオプショナルなPropsにデフォルト値を設定
  • イミュータビリティ:Propsは読み取り専用、変更は親コンポーネントで行う

Propsを正しく理解することで、再利用可能で保守しやすいコンポーネントを設計できます。次の記事では、コンポーネント内部で変化するデータを管理する「useState」について解説します。

参考リンク