Chapter 03

Props

コンポーネントに外からデータを渡す仕組み「Props」を学びます。コンポーネントを汎用的に再利用できるようになります。

40 min

このチャプターで学ぶこと

  • Propsを使ってコンポーネントに値を渡せる
  • 分割代入(デストラクチャリング)でPropsを受け取れる
  • デフォルト値を設定できる
  • childrenプロップを使って子要素を受け取れる
  • ProfileコンポーネントをPropsで汎用化して複数人のデータを表示できる

Props

前のチャプターで Profile.jsx に「山田太郎」という名前をハードコードしました。このままでは同じコンポーネントを使って別の人のプロフィールを表示できません。そこで登場するのが Props(プロップス) です。

Propsとは

PropsはProperties(プロパティ)の略です。コンポーネントに外からデータを渡す仕組みです。

Laravelのbladeコンポーネントでいえば、@component('card', ['title' => '記事タイトル']) のように変数を渡す感覚に近いです。HTML要素の属性のようにコンポーネントにデータを渡せます。

// 使う側(親コンポーネント)
function App() {
  return (
    <div>
      {/* name と age を Props として渡す */}
      <Profile name="山田太郎" age={25} />
    </div>
  )
}
// 受け取る側(子コンポーネント)
function Profile(props) {
  // props は { name: '山田太郎', age: 25 } というオブジェクト
  return (
    <div>
      <h2>{props.name}</h2>
      <p>{props.age}</p>
    </div>
  )
}

コンポーネントは引数 props を受け取ります。props は渡された全データが入ったJavaScriptオブジェクトです。

Propsのデータ型

文字列は引用符 "" で、それ以外はすべて {} で渡します。

function App() {
  const userData = { hobby: 'プログラミング' }

  return (
    <UserCard
      name="山田太郎"           // 文字列は "" で渡せる
      age={25}                  // 数値は {} が必要
      isAdmin={true}            // 真偽値も {}
      hobbies={['読書', '映画']} // 配列も {}
      profile={userData}        // オブジェクトも {}
      onClose={() => {}}        // 関数も {} で渡せる(後のチャプターで詳しく)
    />
  )
}
📝 メモ

isActive={true}isActive と短縮できます。真偽値のPropsを属性として書くと true になります。Laravelのコンポーネント属性と同じ感覚です。

分割代入(デストラクチャリング)で受け取る

毎回 props.nameprops.age と書くのは冗長です。JavaScriptの分割代入を使うとすっきり書けます。

// props オブジェクトをそのまま受け取る(冗長)
function Profile(props) {
  return (
    <div>
      <h2>{props.name}</h2>
      <p>{props.age}</p>
      <p>{props.bio}</p>
    </div>
  )
}

// 分割代入で受け取る(こちらが一般的)
function Profile({ name, age, bio }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>{age}</p>
      <p>{bio}</p>
    </div>
  )
}

引数部分で { name, age, bio } と書くだけです。Propsがすぐに変数として使えてスッキリします。実際の現場のコードはほぼこの書き方です。

デフォルト値を設定する

Propsが渡されなかった場合のデフォルト値を設定できます。これも分割代入の機能を使います。

function Profile({ name, age, bio = '自己紹介文はまだ書いていません' }) {
  return (
    <div>
      <h2>{name}</h2>
      {/* age が渡されていない場合は表示しない */}
      {age && <p>{age}</p>}
      <p>{bio}</p>
    </div>
  )
}
function App() {
  return (
    <div>
      {/* bio を渡さなくてもデフォルト値が使われる */}
      <Profile name="田中花子" age={30} />

      {/* bio を渡せば上書きされる */}
      <Profile name="鈴木一郎" age={28} bio="フルスタックエンジニアです" />
    </div>
  )
}
💡 ヒント

デフォルト値は「そのPropsが undefined のとき」に適用されます。null を渡すとデフォルト値は使われません。これはJavaScriptの分割代入の仕様です。

Propsは読み取り専用

重要なルールがあります:Propsは読み取り専用です。受け取ったPropsを変更してはいけません。

// 絶対にやってはいけない
function Profile({ name }) {
  name = name.toUpperCase() // NG!Props を書き換えている
  return <h2>{name}</h2>
}

// 正しい書き方
function Profile({ name }) {
  const displayName = name.toUpperCase() // 新しい変数に入れる
  return <h2>{displayName}</h2>
}

Propsを変更しようとするとReactが警告を出します。コンポーネントは受け取ったデータを表示するだけで、データの変更は親コンポーネントの責任です(これを「一方向データフロー」と呼びます)。

children プロップ

特別なPropとして children があります。コンポーネントのタグで囲んだ内側のJSXchildren として渡されます。

// children を受け取るコンポーネント
function Card({ title, children }) {
  return (
    <div style={{
      border: '1px solid #ddd',
      borderRadius: '8px',
      padding: '1.5rem',
      marginBottom: '1rem'
    }}>
      <h3 style={{ marginTop: 0 }}>{title}</h3>
      {/* ここに子要素がレンダリングされる */}
      {children}
    </div>
  )
}
// 使う側:タグで囲んだ内容が children になる
function App() {
  return (
    <div>
      <Card title="React学習メモ">
        <p>コンポーネントは再利用可能なUI部品です</p>
        <p>Propsで外からデータを渡せます</p>
      </Card>

      <Card title="今日のタスク">
        <ul>
          <li>Propsを理解する</li>
          <li>演習問題を解く</li>
        </ul>
      </Card>
    </div>
  )
}

children を使うとレイアウトやラッパーコンポーネントを作れます。「外枠のデザインは固定で、中身は自由に変えたい」という場面で非常に便利です。

📝 メモ

children はどんなJSXでも受け取れます。テキスト、要素、コンポーネント、配列など何でもOKです。Laravelのbladeでいえば @yield('content') に相当します。

実践:汎用的なコンポーネントを作る

ここで前のチャプターの Profile コンポーネントをPropsを使って汎用化しましょう。

まず、Propsで受け取れるように Profile.jsx を書き直します:

// src/components/Profile.jsx
function Profile({ name, bio, imageUrl, hobbies = [] }) {
  return (
    <section style={{
      display: 'flex',
      gap: '1.5rem',
      padding: '1.5rem',
      borderBottom: '1px solid #eee'
    }}>
      {/* 画像が渡された場合のみ表示 */}
      {imageUrl && (
        <img
          src={imageUrl}
          alt={`${name}のプロフィール写真`}
          style={{
            width: '80px',
            height: '80px',
            borderRadius: '50%',
            objectFit: 'cover',
            flexShrink: 0
          }}
        />
      )}
      <div>
        <h2 style={{ margin: '0 0 0.5rem' }}>{name}</h2>
        <p style={{ margin: '0 0 0.75rem', color: '#666' }}>{bio}</p>
        {hobbies.length > 0 && (
          <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
            {hobbies.map((hobby) => (
              <span key={hobby} style={{
                background: '#eff6ff',
                color: '#2563eb',
                padding: '0.2rem 0.75rem',
                borderRadius: '999px',
                fontSize: '0.875rem'
              }}>
                {hobby}
              </span>
            ))}
          </div>
        )}
      </div>
    </section>
  )
}

export default Profile

App.jsx で同じコンポーネントを使って複数人のプロフィールを表示します:

// src/App.jsx
import Header from './components/Header'
import Profile from './components/Profile'
import Footer from './components/Footer'

// プロフィールデータを配列で定義
const members = [
  {
    id: 1,
    name: '山田太郎',
    bio: 'バックエンドエンジニア。Laravelが得意です。',
    imageUrl: 'https://i.pravatar.cc/150?img=1',
    hobbies: ['Laravel', 'MySQL', 'コーヒー'],
  },
  {
    id: 2,
    name: '田中花子',
    bio: 'フロントエンドエンジニア。ReactとTypeScriptを使っています。',
    imageUrl: 'https://i.pravatar.cc/150?img=5',
    hobbies: ['React', 'TypeScript', 'デザイン'],
  },
  {
    id: 3,
    name: '鈴木一郎',
    bio: 'インフラエンジニア。最近フロントも勉強中です。',
    imageUrl: 'https://i.pravatar.cc/150?img=8',
    hobbies: ['Docker', 'Kubernetes', 'Go'],
  },
]

function App() {
  return (
    <>
      <Header />
      <main style={{ maxWidth: '700px', margin: '0 auto' }}>
        {/* 配列から map でコンポーネントをレンダリング */}
        {members.map((member) => (
          <Profile
            key={member.id}
            name={member.name}
            bio={member.bio}
            imageUrl={member.imageUrl}
            hobbies={member.hobbies}
          />
        ))}
      </main>
      <Footer />
    </>
  )
}

export default App
💡 ヒント

members.map((member) => <Profile key={member.id} {...member} />) のようにスプレッド演算子 {...member} を使うと、オブジェクトのプロパティをまとめてPropsとして渡せます。コードが短くなりますが、どのPropsが渡っているか分かりにくくなるため、チームの方針に合わせて使いましょう。

✍ やってみよう:3人のプロフィールを表示する

上のコードを実装して、3人のプロフィールが表示されることを確認しましょう。

さらに余裕があれば以下に挑戦してみてください:

発展課題 1: Card ラッパーコンポーネントを作る

children プロップを使った Card コンポーネントを作り、ProfileCard で囲んでみましょう。

// src/components/Card.jsx
function Card({ children }) {
  return (
    <div style={{
      border: '1px solid #e2e8f0',
      borderRadius: '12px',
      marginBottom: '1rem',
      overflow: 'hidden',
      boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
    }}>
      {children}
    </div>
  )
}

export default Card
// App.jsx での使い方
{members.map((member) => (
  <Card key={member.id}>
    <Profile
      name={member.name}
      bio={member.bio}
      imageUrl={member.imageUrl}
      hobbies={member.hobbies}
    />
  </Card>
))}

発展課題 2: Propsにデフォルト値を追加する

Profile コンポーネントで imageUrl が渡されなかった場合に、デフォルトのアバター画像(プレースホルダー)を表示してみましょう。

function Profile({ name, bio, imageUrl = 'https://i.pravatar.cc/150?img=0', hobbies = [] }) {
  // ...
}

完成したらこんな画面になります:

localhost:5173
3人のプロフィールカードが縦に並んでいる。各カードに写真、名前、自己紹介文、趣味タグが表示されている
同じProfileコンポーネントを異なるPropsで3回使った結果

Propsのよくある間違い

最後に、Propsでよく起きるミスをまとめておきます。

// よくある間違い1:数値を文字列として渡す
<Profile age="25" />   // age は数値なのに文字列になってしまう
<Profile age={25} />   // 正しい

// よくある間違い2:Props名のタイポ
function Profile({ naem }) { ... }  // 'name' のタイポ
<Profile name="山田" />             // naem は undefined になる

// よくある間違い3:Propsを直接変更する
function Profile({ user }) {
  user.name = '変更後'  // NG!
}

TypeScriptを使うとこういったミスをエディタが検出してくれます。TypeScriptへの移行は少し先のチャプターで扱います。

まとめ

Propsのポイントをまとめます:

これでコンポーネントを汎用的に使い回せるようになりました。でもまだ問題があります。ユーザーがボタンをクリックしたり入力したりしたとき、コンポーネントの表示を動的に変えたいですよね。

次のチャプターでは State(状態) を学びます。コンポーネントが「自分のデータ」を持って、それに応じて表示を更新できるようになります。