10日で覚えるReactDay 3: コンポーネントとProps
books.chapter 310日で覚えるReact

Day 3: コンポーネントとProps

今日学ぶこと

  • コンポーネントの作成と構成方法
  • Propsを使ったデータの受け渡し
  • childrenプロパティの活用
  • コンポーネント分割の指針
  • デフォルトPropsの設定

コンポーネントとは

コンポーネントは、UIを独立した再利用可能な部品に分割したものです。ReactアプリケーションはコンポーネントのツリーとしてUI構築されます。

flowchart TB
    subgraph Tree["コンポーネントツリー"]
        App["App"]
        Header["Header"]
        Main["Main"]
        Footer["Footer"]
        Nav["Nav"]
        Logo["Logo"]
        Article["Article"]
        Sidebar["Sidebar"]
    end

    App --> Header
    App --> Main
    App --> Footer
    Header --> Logo
    Header --> Nav
    Main --> Article
    Main --> Sidebar

    style App fill:#3b82f6,color:#fff
    style Header fill:#8b5cf6,color:#fff
    style Main fill:#8b5cf6,color:#fff
    style Footer fill:#8b5cf6,color:#fff

コンポーネントの作成

Reactでは、関数を使ってコンポーネントを作成します。

// シンプルなコンポーネント
function Welcome() {
  return <h1>ようこそ!</h1>;
}

// コンポーネントを使用
function App() {
  return (
    <div>
      <Welcome />
      <Welcome />
      <Welcome />
    </div>
  );
}
TypeScript版
// シンプルなコンポーネント
function Welcome() {
  return <h1>ようこそ!</h1>;
}

// コンポーネントを使用
function App() {
  return (
    <div>
      <Welcome />
      <Welcome />
      <Welcome />
    </div>
  );
}

コンポーネントの命名規則

ルール 説明
大文字で始める WelcomeUserCard(小文字はHTMLタグと解釈される)
PascalCase 複数単語は UserProfileCard のように
意味のある名前 機能を表す名前をつける

Propsとは

Props (Properties) は、親コンポーネントから子コンポーネントへデータを渡す仕組みです。

flowchart LR
    subgraph Flow["Propsの流れ"]
        Parent["親コンポーネント<br/>App"]
        Child["子コンポーネント<br/>Greeting"]
    end

    Parent -->|"name='太郎'"| Child

    style Parent fill:#3b82f6,color:#fff
    style Child fill:#22c55e,color:#fff

Propsの基本的な使い方

// 子コンポーネント - propsを受け取る
function Greeting(props) {
  return <h1>こんにちは、{props.name}さん!</h1>;
}

// 親コンポーネント - propsを渡す
function App() {
  return (
    <div>
      <Greeting name="太郎" />
      <Greeting name="花子" />
      <Greeting name="次郎" />
    </div>
  );
}
TypeScript版
// 子コンポーネント - propsを受け取る
interface GreetingProps {
  name: string;
}

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

// 親コンポーネント - propsを渡す
function App() {
  return (
    <div>
      <Greeting name="太郎" />
      <Greeting name="花子" />
      <Greeting name="次郎" />
    </div>
  );
}

分割代入でPropsを受け取る

より簡潔に書くために、分割代入を使用します。

// 分割代入を使用
function Greeting({ name }) {
  return <h1>こんにちは、{name}さん!</h1>;
}

// 複数のpropsを受け取る
function UserCard({ name, age, email }) {
  return (
    <div className="user-card">
      <h2>{name}</h2>
      <p>年齢: {age}</p>
      <p>メール: {email}</p>
    </div>
  );
}

// 使用例
<UserCard name="太郎" age={25} email="taro@example.com" />
TypeScript版
// 分割代入を使用
interface GreetingProps {
  name: string;
}

function Greeting({ name }: GreetingProps) {
  return <h1>こんにちは、{name}さん!</h1>;
}

// 複数のpropsを受け取る
interface UserCardProps {
  name: string;
  age: number;
  email: string;
}

function UserCard({ name, age, email }: UserCardProps) {
  return (
    <div className="user-card">
      <h2>{name}</h2>
      <p>年齢: {age}</p>
      <p>メール: {email}</p>
    </div>
  );
}

// 使用例
<UserCard name="太郎" age={25} email="taro@example.com" />

Propsの型

Propsには様々な型の値を渡せます。

function Example({
  text,        // 文字列
  count,       // 数値
  isActive,    // 真偽値
  items,       // 配列
  user,        // オブジェクト
  onClick,     // 関数
}) {
  return (
    <div>
      <p>{text}</p>
      <p>カウント: {count}</p>
      <p>状態: {isActive ? 'アクティブ' : '非アクティブ'}</p>
      <ul>
        {items.map((item, i) => <li key={i}>{item}</li>)}
      </ul>
      <p>ユーザー: {user.name}</p>
      <button onClick={onClick}>クリック</button>
    </div>
  );
}

// 使用例
<Example
  text="こんにちは"
  count={42}
  isActive={true}
  items={['A', 'B', 'C']}
  user={{ name: '太郎', age: 25 }}
  onClick={() => alert('クリックされました')}
/>
TypeScript版
interface User {
  name: string;
}

interface ExampleProps {
  text: string;
  count: number;
  isActive: boolean;
  items: string[];
  user: User;
  onClick: () => void;
}

function Example({
  text,
  count,
  isActive,
  items,
  user,
  onClick,
}: ExampleProps) {
  return (
    <div>
      <p>{text}</p>
      <p>カウント: {count}</p>
      <p>状態: {isActive ? 'アクティブ' : '非アクティブ'}</p>
      <ul>
        {items.map((item, i) => <li key={i}>{item}</li>)}
      </ul>
      <p>ユーザー: {user.name}</p>
      <button onClick={onClick}>クリック</button>
    </div>
  );
}

// 使用例
<Example
  text="こんにちは"
  count={42}
  isActive={true}
  items={['A', 'B', 'C']}
  user={{ name: '太郎', age: 25 }}
  onClick={() => alert('クリックされました')}
/>

childrenプロパティ

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

// Cardコンポーネント
function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

// 使用例
function App() {
  return (
    <Card>
      <h2>タイトル</h2>
      <p>これはカードの内容です。</p>
    </Card>
  );
}
TypeScript版
import { ReactNode } from 'react';

// Cardコンポーネント
interface CardProps {
  children: ReactNode;
}

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

// 使用例
function App() {
  return (
    <Card>
      <h2>タイトル</h2>
      <p>これはカードの内容です。</p>
    </Card>
  );
}
flowchart TB
    subgraph Children["childrenの仕組み"]
        Card["Card<br/>className='card'"]
        Content["children<br/>(h2 + p)"]
    end

    Card --> Content

    style Card fill:#3b82f6,color:#fff
    style Content fill:#22c55e,color:#fff

childrenの活用例

// レイアウトコンポーネント
function PageLayout({ children }) {
  return (
    <div className="page">
      <header>ヘッダー</header>
      <main>{children}</main>
      <footer>フッター</footer>
    </div>
  );
}

// ボタンコンポーネント
function Button({ children, onClick }) {
  return (
    <button className="btn" onClick={onClick}>
      {children}
    </button>
  );
}

// 使用例
<PageLayout>
  <h1>ようこそ</h1>
  <Button onClick={() => alert('クリック')}>
    詳しく見る
  </Button>
</PageLayout>
TypeScript版
import { ReactNode } from 'react';

// レイアウトコンポーネント
interface PageLayoutProps {
  children: ReactNode;
}

function PageLayout({ children }: PageLayoutProps) {
  return (
    <div className="page">
      <header>ヘッダー</header>
      <main>{children}</main>
      <footer>フッター</footer>
    </div>
  );
}

// ボタンコンポーネント
interface ButtonProps {
  children: ReactNode;
  onClick: () => void;
}

function Button({ children, onClick }: ButtonProps) {
  return (
    <button className="btn" onClick={onClick}>
      {children}
    </button>
  );
}

// 使用例
<PageLayout>
  <h1>ようこそ</h1>
  <Button onClick={() => alert('クリック')}>
    詳しく見る
  </Button>
</PageLayout>

コンポーネントの合成

小さなコンポーネントを組み合わせて、複雑なUIを構築します。

// アバターコンポーネント
function Avatar({ src, alt }) {
  return <img className="avatar" src={src} alt={alt} />;
}

// ユーザー情報コンポーネント
function UserInfo({ name, title }) {
  return (
    <div className="user-info">
      <p className="name">{name}</p>
      <p className="title">{title}</p>
    </div>
  );
}

// コメントコンポーネント(合成)
function Comment({ author, text, date }) {
  return (
    <div className="comment">
      <Avatar src={author.avatarUrl} alt={author.name} />
      <UserInfo name={author.name} title={author.title} />
      <p className="comment-text">{text}</p>
      <p className="comment-date">{date}</p>
    </div>
  );
}
TypeScript版
// アバターコンポーネント
interface AvatarProps {
  src: string;
  alt: string;
}

function Avatar({ src, alt }: AvatarProps) {
  return <img className="avatar" src={src} alt={alt} />;
}

// ユーザー情報コンポーネント
interface UserInfoProps {
  name: string;
  title: string;
}

function UserInfo({ name, title }: UserInfoProps) {
  return (
    <div className="user-info">
      <p className="name">{name}</p>
      <p className="title">{title}</p>
    </div>
  );
}

// コメントコンポーネント(合成)
interface Author {
  avatarUrl: string;
  name: string;
  title: string;
}

interface CommentProps {
  author: Author;
  text: string;
  date: string;
}

function Comment({ author, text, date }: CommentProps) {
  return (
    <div className="comment">
      <Avatar src={author.avatarUrl} alt={author.name} />
      <UserInfo name={author.name} title={author.title} />
      <p className="comment-text">{text}</p>
      <p className="comment-date">{date}</p>
    </div>
  );
}
flowchart TB
    subgraph Composition["コンポーネントの合成"]
        Comment["Comment"]
        Avatar["Avatar"]
        UserInfo["UserInfo"]
        Text["コメント本文"]
        Date["日付"]
    end

    Comment --> Avatar
    Comment --> UserInfo
    Comment --> Text
    Comment --> Date

    style Comment fill:#3b82f6,color:#fff
    style Avatar fill:#22c55e,color:#fff
    style UserInfo fill:#22c55e,color:#fff

コンポーネント分割の指針

いつコンポーネントを分割すべきか

指標 説明
再利用性 同じUIが複数箇所で使われる
複雑さ コンポーネントが大きくなりすぎている
責任の分離 異なる関心事が混在している
テスト容易性 独立してテストしたい部分がある

例: 分割前と分割後

// ❌ 分割前: 1つのコンポーネントに詰め込みすぎ
function ProductPage() {
  return (
    <div>
      <header>
        <img src="/logo.png" alt="ロゴ" />
        <nav>
          <a href="/">ホーム</a>
          <a href="/products">商品</a>
        </nav>
      </header>
      <main>
        <div className="product">
          <img src="/product.jpg" alt="商品画像" />
          <h1>商品名</h1>
          <p>¥1,000</p>
          <button>カートに追加</button>
        </div>
        <div className="reviews">
          <h2>レビュー</h2>
          {/* レビューリスト */}
        </div>
      </main>
      <footer>© 2024</footer>
    </div>
  );
}
// ✅ 分割後: 責任ごとにコンポーネントを分離
function Logo() {
  return <img src="/logo.png" alt="ロゴ" />;
}

function Navigation() {
  return (
    <nav>
      <a href="/">ホーム</a>
      <a href="/products">商品</a>
    </nav>
  );
}

function Header() {
  return (
    <header>
      <Logo />
      <Navigation />
    </header>
  );
}

function ProductDetail({ product }) {
  return (
    <div className="product">
      <img src={product.image} alt={product.name} />
      <h1>{product.name}</h1>
      <p>¥{product.price}</p>
      <button>カートに追加</button>
    </div>
  );
}

function ReviewList({ reviews }) {
  return (
    <div className="reviews">
      <h2>レビュー</h2>
      {reviews.map(review => (
        <ReviewItem key={review.id} review={review} />
      ))}
    </div>
  );
}

function Footer() {
  return <footer>© 2024</footer>;
}

function ProductPage({ product, reviews }) {
  return (
    <div>
      <Header />
      <main>
        <ProductDetail product={product} />
        <ReviewList reviews={reviews} />
      </main>
      <Footer />
    </div>
  );
}
TypeScript版
function Logo() {
  return <img src="/logo.png" alt="ロゴ" />;
}

function Navigation() {
  return (
    <nav>
      <a href="/">ホーム</a>
      <a href="/products">商品</a>
    </nav>
  );
}

function Header() {
  return (
    <header>
      <Logo />
      <Navigation />
    </header>
  );
}

interface Product {
  image: string;
  name: string;
  price: number;
}

interface ProductDetailProps {
  product: Product;
}

function ProductDetail({ product }: ProductDetailProps) {
  return (
    <div className="product">
      <img src={product.image} alt={product.name} />
      <h1>{product.name}</h1>
      <p>¥{product.price}</p>
      <button>カートに追加</button>
    </div>
  );
}

interface Review {
  id: number;
  text: string;
}

interface ReviewListProps {
  reviews: Review[];
}

function ReviewList({ reviews }: ReviewListProps) {
  return (
    <div className="reviews">
      <h2>レビュー</h2>
      {reviews.map(review => (
        <ReviewItem key={review.id} review={review} />
      ))}
    </div>
  );
}

function Footer() {
  return <footer>&copy; 2024</footer>;
}

interface ProductPageProps {
  product: Product;
  reviews: Review[];
}

function ProductPage({ product, reviews }: ProductPageProps) {
  return (
    <div>
      <Header />
      <main>
        <ProductDetail product={product} />
        <ReviewList reviews={reviews} />
      </main>
      <Footer />
    </div>
  );
}

デフォルトProps

propsにデフォルト値を設定できます。

// 方法1: デフォルト引数を使用(推奨)
function Button({ text = 'クリック', color = 'blue' }) {
  return (
    <button style={{ backgroundColor: color }}>
      {text}
    </button>
  );
}

// 使用例
<Button />                    // デフォルト値が使われる
<Button text="送信" />        // textのみ上書き
<Button color="red" />        // colorのみ上書き
<Button text="削除" color="red" />  // 両方上書き
TypeScript版
// 方法1: デフォルト引数を使用(推奨)
interface ButtonProps {
  text?: string;
  color?: string;
}

function Button({ text = 'クリック', color = 'blue' }: ButtonProps) {
  return (
    <button style={{ backgroundColor: color }}>
      {text}
    </button>
  );
}

// 使用例
<Button />                    // デフォルト値が使われる
<Button text="送信" />        // textのみ上書き
<Button color="red" />        // colorのみ上書き
<Button text="削除" color="red" />  // 両方上書き
// 方法2: OR演算子を使用
function Greeting({ name }) {
  return <h1>こんにちは、{name || 'ゲスト'}さん!</h1>;
}

// 方法3: Nullish合体演算子を使用
function Counter({ count }) {
  return <p>カウント: {count ?? 0}</p>;
}

デフォルト値の使い分け

方法 使いどころ
デフォルト引数 基本的にはこれを使う
OR演算子 || falsy値(0, ''など)もデフォルトにしたい場合
Nullish合体 ?? null/undefinedのみデフォルトにしたい場合

Propsのスプレッド構文

オブジェクトのプロパティをpropsとして一括で渡せます。

function UserProfile({ name, age, email, avatar }) {
  return (
    <div>
      <img src={avatar} alt={name} />
      <h2>{name}</h2>
      <p>{age}</p>
      <p>{email}</p>
    </div>
  );
}

// 通常の方法
const user = { name: '太郎', age: 25, email: 'taro@example.com', avatar: '/taro.jpg' };

<UserProfile
  name={user.name}
  age={user.age}
  email={user.email}
  avatar={user.avatar}
/>

// スプレッド構文を使用
<UserProfile {...user} />
TypeScript版
interface UserProfileProps {
  name: string;
  age: number;
  email: string;
  avatar: string;
}

function UserProfile({ name, age, email, avatar }: UserProfileProps) {
  return (
    <div>
      <img src={avatar} alt={name} />
      <h2>{name}</h2>
      <p>{age}</p>
      <p>{email}</p>
    </div>
  );
}

// 通常の方法
const user: UserProfileProps = { name: '太郎', age: 25, email: 'taro@example.com', avatar: '/taro.jpg' };

<UserProfile
  name={user.name}
  age={user.age}
  email={user.email}
  avatar={user.avatar}
/>

// スプレッド構文を使用
<UserProfile {...user} />

一部のpropsを取り出す

function Button({ children, className, ...rest }) {
  return (
    <button className={`btn ${className}`} {...rest}>
      {children}
    </button>
  );
}

// 使用例
<Button className="primary" onClick={handleClick} disabled={true}>
  送信
</Button>
TypeScript版
import { ReactNode, ComponentPropsWithoutRef } from 'react';

interface ButtonProps extends ComponentPropsWithoutRef<'button'> {
  children: ReactNode;
  className?: string;
}

function Button({ children, className, ...rest }: ButtonProps) {
  return (
    <button className={`btn ${className}`} {...rest}>
      {children}
    </button>
  );
}

// 使用例
<Button className="primary" onClick={handleClick} disabled={true}>
  送信
</Button>

まとめ

概念 説明
コンポーネント UIを構成する独立した再利用可能な部品
Props 親から子へデータを渡す仕組み
children タグ間のコンテンツを受け取る特別なprops
コンポーネント合成 小さなコンポーネントを組み合わせてUIを構築
デフォルトProps propsの初期値を設定

重要ポイント

  1. コンポーネント名は大文字で始める
  2. Propsは読み取り専用(子コンポーネントで変更してはいけない)
  3. children柔軟なコンポーネントを作れる
  4. コンポーネントは小さく保つことで保守性が向上
  5. デフォルト引数でpropsの初期値を設定できる

練習問題

問題1: 基本

以下のpropsを受け取るProfileCardコンポーネントを作成してください:

  • name(名前)
  • job(職業)
  • bio(自己紹介文)

問題2: children

Panelコンポーネントを作成してください。タイトルとchildrenを受け取り、装飾されたパネルとして表示します。

<Panel title="お知らせ">
  <p>明日は休業日です。</p>
</Panel>

チャレンジ問題

以下の要件を満たすProductCardコンポーネントを作成してください:

  • 商品名、価格、画像URL、在庫数を受け取る
  • 在庫がない場合は「売り切れ」バッジを表示
  • 価格が10000円以上の場合は「高額商品」バッジを表示

参考リンク


次回予告: Day 4では「Stateとイベント処理」について学びます。ユーザーの操作に反応するインタラクティブなUIを作成しましょう。