React基礎講座 #4 コンポーネントとprops

読了 9分

前回はJSX構文を見てきました。その過程で私たちは自然とAppという名前の関数を見ました。実はこのAppこそがReactのもっとも重要な単位であるコンポーネント(Component) です。今回はコンポーネントとは何で、どう作るのか、そしてコンポーネント同士でデータをやり取りする通路であるpropsについて見ていくことにしましょう。

なぜコンポーネントが必要なのですか? #

画面全体を1つの巨大な関数にすべて書くと想像してみてください。ヘッダー、サイドバー、メインコンテンツ、フッター、ボタン、入力欄…コードはあっという間に数百、数千行になり、どこを直せばいいのか探すことすら難しくなります。同じ形のボタンが画面に10個あれば同じコードも10回書かなければなりません。

Reactはこの問題をコンポーネントという概念で解決します。コンポーネントは画面の1つの断片を表現する再利用可能な単位です。ヘッダー、ボタン、カード、入力欄のような画面要素をそれぞれ1つのコンポーネントとして作っておけば、必要なところでまるでHTMLタグのように持ってきて使えます。

最初のコンポーネントを作る #

Reactでコンポーネントは結局JSXを返すJavaScriptの関数です。私たちが前回ずっと見てきたAppもそうです。

src/App.jsx
function Greeting() {
  return <h1>こんにちはReact!</h1>;
}

function App() {
  return (
    <div>
      <Greeting />
    </div>
  );
}

export default App;

Greetingという新しい関数を定義して、Appの中でまるでHTMLタグのように<Greeting />を使いました。こうして関数1つがそのまま1つのコンポーネントになります。

注記
コンポーネント名は必ず大文字で始めなければなりません<greeting />のように小文字で始めるとReactはこれを通常のHTMLタグとして認識し、意図と異なる動作をします。命名規則: PascalCase(UserCardLoginButton)が標準です。

コンポーネントを別ファイルに分離する #

コンポーネントが増えると1つのファイルにすべて書くよりファイルごとに分けた方がよいです。普通はコンポーネント1つにつきファイル1つで作ります。

src/Greeting.jsxというファイルを新しく作って次のように書きます。

src/Greeting.jsx
function Greeting() {
  return <h1>こんにちはReact!</h1>;
}

export default Greeting;

そしてApp.jsxではこのように取り込みます。

src/App.jsx
import Greeting from './Greeting';

function App() {
  return (
    <div>
      <Greeting />
    </div>
  );
}

export default App;

export defaultで送り出してimportで受け取る一般的なJavaScriptモジュールパターンそのままです。ファイル拡張子(.jsx)は省略してもViteが自動で見つけてくれます。

ヒント
ファイル名もコンポーネント名と同じくPascalCaseで付けるのが慣例です。Greeting.jsxUserCard.jsxのように。フォルダ構造はプロジェクトごとに異なりますが、小さなプロジェクトはsrc/components/配下にまとめておくのが一般的です。

コンポーネントにデータを渡す — props #

Greetingコンポーネントは常に「こんにちは、React!」だけを出力します。ところがユーザーごとに異なる挨拶を見せたい場合はどうすればよいでしょうか?propsを使えばよいのです。

propsはコンポーネントの仮引数のような概念です。親が子コンポーネントにデータを下ろすときに使います。HTML属性を使うように自然に書けばよいです。

src/App.jsx
import Greeting from './Greeting';

function App() {
  return (
    <div>
      <Greeting name="チョルス" />
      <Greeting name="ヨンヒ" />
      <Greeting name="ミンス" />
    </div>
  );
}

export default App;

Greetingを3回使いながら、毎回異なるname値を渡しました。それではGreetingコンポーネントがこの値を受け取れるように修正します。

src/Greeting.jsx
function Greeting(props) {
  return <h1>こんにちは{props.name}さん!</h1>;
}

export default Greeting;

関数の最初の仮引数としてpropsというオブジェクトが入ってきます。親が渡したすべての属性がこのオブジェクトのプロパティに収まります。name="チョルス"で渡したのでprops.name'チョルス'です。

画面には次のように出力されます。

画面出力
こんにちは、チョルスさん!
こんにちは、ヨンヒさん!
こんにちは、ミンスさん!

同じコンポーネントがpropsだけ変えて3回再利用されたわけです。これがコンポーネントの核心的な価値です。

様々なタイプのprops #

propsには文字列だけでなく、数値、ブーリアン、配列、オブジェクト、さらには関数まで渡すことができます。文字列でない値は波括弧{ }で包まなければならない、という点だけ覚えておいてください。

src/App.jsx
function App() {
  const user = { name: 'チョルス', email: 'cheolsu@example.com' };

  return (
    <UserCard
      name="チョルス"
      age={30}
      isAdmin={true}
      hobbies={['読書', 'コーディング', '旅行']}
      profile={user}
    />
  );
}
src/UserCard.jsx
function UserCard(props) {
  return (
    <div>
      <h2>{props.name} ({props.age})</h2>
      {props.isAdmin && <p>管理者権限があります</p>}
      <p>メール: {props.profile.email}</p>
      <p>趣味: {props.hobbies.join(', ')}</p>
    </div>
  );
}

export default UserCard;

文字列はクォートで(name="チョルス")、それ以外のJavaScript値は波括弧で(age={30})渡すと覚えておけばよいです。

分割代入できれいに受け取る #

props.nameprops.ageのように毎回props.を付けるのが面倒なら、JavaScriptの分割代入(destructuring) を使えます。

src/Greeting.jsx
function Greeting({ name }) {
  return <h1>こんにちは{name}さん!</h1>;
}

export default Greeting;

仮引数の位置で直接分解して受け取ると、関数本文ではnameだけで使えるのでコードが短くなります。複数のpropsならそのまま並べればよいです。

src/UserCard.jsx
function UserCard({ name, age, isAdmin, hobbies, profile }) {
  return (
    <div>
      <h2>{name} ({age})</h2>
      {isAdmin && <p>管理者権限があります</p>}
      <p>メール: {profile.email}</p>
      <p>趣味: {hobbies.join(', ')}</p>
    </div>
  );
}

export default UserCard;

実際のReactコードではこの方式の方がはるかによく見かけます。今後私たちもこのスタイルで書きます。

デフォルト値を指定する #

propが渡されないかもしれない状況なら、分割代入と一緒にデフォルト値を指定できます。

src/Greeting.jsx
function Greeting({ name = 'お客様' }) {
  return <h1>こんにちは{name}さん!</h1>;
}

<Greeting />のようにnameなしで使うと自動で'お客様'が使われます。

children — コンポーネントの間に入る子 #

これまで私たちは<Greeting />のように自己閉じの形でしかコンポーネントを使いませんでした。ところがHTMLのように開きタグと閉じタグの間に何かを入れたいときがあります。

src/App.jsx
import Card from './Card';

function App() {
  return (
    <Card>
      <h2>お知らせ</h2>
      <p>本日は休業日です</p>
    </Card>
  );
}

このとき<Card></Card>の間に入った内容は自動でchildrenという特別なpropに収められて渡されます。

src/Card.jsx
function Card({ children }) {
  return (
    <div className="card" style={{ border: '1px solid #ccc', padding: '16px', borderRadius: '8px' }}>
      {children}
    </div>
  );
}

export default Card;

Cardコンポーネントは中に何が入るか分かりませんが、childrenをそこに出力するだけでよいのです。このパターンはレイアウト(Card、Modal、Layoutなど)やラッパー(Wrapper)的性格のコンポーネントで非常によく使われます。

propsは読み取り専用です #

最後にもっとも重要なルールです。コンポーネントは自分が受け取ったpropsを絶対に修正してはいけません。次のコードは間違った例です。

間違った例
function Greeting({ name }) {
  name = name.toUpperCase(); // 🚫 propsを直接修正
  return <h1>こんにちは{name}さん!</h1>;
}

propsは親から流れ落ちてくるデータのコピーであり、子が勝手に変えられる値ではありません。加工が必要なら新しい変数に入れて使ってください。

正しい例
function Greeting({ name }) {
  const upperName = name.toUpperCase();
  return <h1>こんにちは{upperName}さん!</h1>;
}
注記
このルールは単なるスタイルガイドではなく、Reactの動作原理に直結します。Reactはデータが上から下へ(親 → 子)一方向に流れるという仮定の上に作られているため、子が受け取ったpropsを勝手に変えるとデータフローが混乱し、デバッグが非常に難しくなります。親のデータを変える必要がある状況では、次の記事で学ぶstateとコールバック関数を活用します。

自分でやってみる #

前回作ったsrc/App.jsxを次の構造に変えてみてください。

src/UserCard.jsxを新しく作って:

src/UserCard.jsx
function UserCard({ name, age, hobbies }) {
  return (
    <div style={{ border: '1px solid #ccc', padding: '12px', margin: '8px', borderRadius: '8px' }}>
      <h2>{name} ({age})</h2>
      <p>趣味: {hobbies.join(', ')}</p>
    </div>
  );
}

export default UserCard;

src/App.jsxはこのように:

src/App.jsx
import UserCard from './UserCard';

function App() {
  return (
    <>
      <h1>会員リスト</h1>
      <UserCard name="チョルス" age={30} hobbies={['読書', 'コーディング']} />
      <UserCard name="ヨンヒ" age={28} hobbies={['旅行', '料理', '写真']} />
      <UserCard name="ミンス" age={35} hobbies={['ゲーム']} />
    </>
  );
}

export default App;

保存すると3人の会員カードが画面に描画されます。同じUserCardコンポーネントがpropsだけ変わりながら3回再利用されたのです。名前や趣味を変えてみたり、新しいカードを追加してみてください。

おわりに #

今回の記事ではReactの核心単位であるコンポーネントを作り、別ファイルに分離し、propsでデータを渡す方法を見てきました。分割代入、デフォルト値、children、propsの読み取り専用ルールまで扱いました。これで画面を小さな断片に分けて再利用できるようになりました。

これまで私たちが扱ったコンポーネントはすべて静的でした。一度描画されたら絶対に変わりませんでした。しかし実際のアプリはユーザー入力に応じて、時間に応じて、サーバーレスポンスに応じて絶えず姿を変えます。次の記事「React基礎講座 #5 StateとuseState」では、コンポーネントが変わりうるデータを扱う方法、つまりstateの概念とuseStateフックを学んでいくことにしましょう。

X