レッスン 4

コンポーネント分割の実践

ReacTouch
React Logo
LESSON 4|

コンポーネント分割の実践

前のレッスンで作った商品カードを、再利用可能なコンポーネントに分割していきます。 少しずつ機能を切り分けて、保守しやすいコンポーネント構造を作りましょう!
Step 1

商品カードコンポーネントを別ファイルに分割しよう!

最初のステップとして、商品カードを別ファイルに分割しましょう。 現在App.jsxに書かれている商品カードのコードを、新しく作るProductCard.jsxファイルに移動させます。 手順: 1. 下の「ProductCard.jsxを作成」ボタンを押して空のファイルを作成する 2. ProductCardコンポーネントとしてexport defaultする 3. App.jsxから商品カードに関連するコードをコピーする 4. App.jsxでProductCardをimportして使用する

App.jsx

import './styles.css'

const App = () => {
  const getStarRating = (rating) => {
    return '★'.repeat(rating) + '☆'.repeat(5 - rating);
  }
  
  const product = {
    name: "スマートウォッチ",
    brand: "TechGear",
    price: 12000,
    discountRate: 0.20,
    rating: 4,
    reviewCount: 128,
    imageUrl: "https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=400&h=533&fit=crop&crop=center",
    altText: "スマートウォッチの商品画像"
  };

  const discountPercent = Math.round(product.discountRate * 100);
  const discountedPrice = product.price - product.price * product.discountRate;
  
  return (
    <div className="product-card">
      <div className="product-image-container">
        <img 
          className="product-image"
          src={product.imageUrl}
          alt={product.altText}
        />
      </div>
      <div className="product-info">
        <p className="brand-name">{product.brand}</p>
        <h1>{product.name}</h1>
        <div className="price-container">
          <p className="discount-price">¥{discountedPrice.toLocaleString()}</p>
          <p className="price">¥{product.price.toLocaleString()}</p>
          <span className="discount-badge">{discountPercent}%OFF</span>
        </div>
        <p className="rating">
          {getStarRating(product.rating)}
          <span className="review-count">({product.reviewCount})</span>
        </p>
      </div>
    </div>
  )
}

export default App
📦 ProductCard.jsxの基本構造
import './styles.css'

const ProductCard = () => {
  // ここに商品カードのロジックを移動
  return (
    // ここに商品カードのJSXを移動
  )
}

export default ProductCard
⚙️ 商品カードの関数・データ
const getStarRating = (rating) => {
  return '★'.repeat(rating) + '☆'.repeat(5 - rating);
}

const product = {
  name: "スマートウォッチ",
  brand: "TechGear",
  price: 12000,
  discountRate: 0.20,
  rating: 4,
  reviewCount: 128,
  imageUrl: "https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=400&h=533&fit=crop&crop=center",
  altText: "スマートウォッチの商品画像"
};

const discountPercent = Math.round(product.discountRate * 100);
const discountedPrice = product.price - product.price * product.discountRate;
🎨 商品カード表示のJSX
return (
  <div className="product-card">
    <div className="product-image-container">
      <img 
        className="product-image"
        src={product.imageUrl}
        alt={product.altText}
      />
    </div>
    <div className="product-info">
      <p className="brand-name">{product.brand}</p>
      <h1>{product.name}</h1>
      <div className="price-container">
        <p className="discount-price">¥{discountedPrice.toLocaleString()}</p>
        <p className="price">¥{product.price.toLocaleString()}</p>
        <span className="discount-badge">{discountPercent}%OFF</span>
      </div>
      <p className="rating">
        {getStarRating(product.rating)}
        <span className="review-count">({product.reviewCount})</span>
      </p>
    </div>
  </div>
)
📥 App.jsxでのimport文
import ProductCard from './ProductCard'

const App = () => {
  return (
    <div>
      <ProductCard />
    </div>
  )
}

export default App
Step 2

画像部分を別コンポーネント化しよう!

次に、商品画像部分を別コンポーネントに分割します。 ProductImage.jsxファイルを作成しましょう。 実装する内容: - 商品画像を表示するProductImageコンポーネント - ProductCard.jsxから呼び出す - 画像関連のスタイルも適用

ProductCard.jsx

import './styles.css'

const ProductCard = () => {
  const getStarRating = (rating) => {
    return '★'.repeat(rating) + '☆'.repeat(5 - rating);
  }
  
  const product = {
    name: "スマートウォッチ",
    brand: "TechGear",
    price: 12000,
    discountRate: 0.20,
    rating: 4,
    reviewCount: 128,
    imageUrl: "https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=400&h=533&fit=crop&crop=center",
    altText: "スマートウォッチの商品画像"
  };

  const discountPercent = Math.round(product.discountRate * 100);
  const discountedPrice = product.price - product.price * product.discountRate;
  
  return (
    <div className="product-card">
      <div className="product-image-container">
        <img 
          className="product-image"
          src={product.imageUrl}
          alt={product.altText}
        />
      </div>
      <div className="product-info">
        <p className="brand-name">{product.brand}</p>
        <h1>{product.name}</h1>
        <div className="price-container">
          <p className="discount-price">¥{discountedPrice.toLocaleString()}</p>
          <p className="price">¥{product.price.toLocaleString()}</p>
          <span className="discount-badge">{discountPercent}%OFF</span>
        </div>
        <p className="rating">
          {getStarRating(product.rating)}
          <span className="review-count">({product.reviewCount})</span>
        </p>
      </div>
    </div>
  )
}

export default ProductCard
🖼️ ProductImageコンポーネントの基本構造
const ProductImage = () => {
  const product = {
    imageUrl: "https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=400&h=533&fit=crop&crop=center",
    altText: "スマートウォッチの商品画像"
  };

  return (
    <div className="product-image-container">
      <img
        className="product-image"
        src={product.imageUrl}
        alt={product.altText}
      />
    </div>
  );
}

export default ProductImage
📦 ProductCardでのProductImageインポート
import ProductImage from './ProductImage'
📦 ProductCardでのProductImageの使用
<ProductImage />
Step 3

商品情報部分を別コンポーネント化しよう!

次に、商品情報部分を別コンポーネントに分割します。 ProductInfo.jsxファイルを作成して、商品情報表示の責任を持たせましょう。 手順: 1. 下の「ProductInfo.jsxを作成」ボタンを押して空のファイルを作成する 2. ProductInfoコンポーネントとしてexport defaultする 3. ProductCard.jsxから商品情報に関連するコードをコピーする 4. getStarRating関数もProductInfo内に移動する 5. ProductCard.jsxでProductInfoをimportして使用する

ProductCard.jsx

import './styles.css'
import ProductImage from './ProductImage'

const ProductCard = () => {
  const getStarRating = (rating) => {
    return '★'.repeat(rating) + '☆'.repeat(5 - rating);
  }
  
  const product = {
    name: "スマートウォッチ",
    brand: "TechGear",
    price: 12000,
    discountRate: 0.20,
    rating: 4,
    reviewCount: 128
  };

  const discountPercent = Math.round(product.discountRate * 100);
  const discountedPrice = product.price - product.price * product.discountRate;
  
  return (
    <div className="product-card">
      <ProductImage />
      <div className="product-info">
        <p className="brand-name">{product.brand}</p>
        <h1>{product.name}</h1>
        <div className="price-container">
          <p className="discount-price">¥{discountedPrice.toLocaleString()}</p>
          <p className="price">¥{product.price.toLocaleString()}</p>
          <span className="discount-badge">{discountPercent}%OFF</span>
        </div>
        <p className="rating">
          {getStarRating(product.rating)}
          <span className="review-count">({product.reviewCount})</span>
        </p>
      </div>
    </div>
  )
}

export default ProductCard
📋 ProductInfoコンポーネントの基本構造
const ProductInfo = () => {
  const getStarRating = (rating) => {
    return '★'.repeat(rating) + '☆'.repeat(5 - rating);
  }
  
  const product = {
    name: "スマートウォッチ",
    brand: "TechGear",
    price: 12000,
    discountRate: 0.20,
    rating: 4,
    reviewCount: 128
  };

  const discountPercent = Math.round(product.discountRate * 100);
  const discountedPrice = product.price - product.price * product.discountRate;

  return (
    <div className="product-info">
      <p className="brand-name">{product.brand}</p>
      <h1>{product.name}</h1>
      <div className="price-container">
        <p className="discount-price">¥{discountedPrice.toLocaleString()}</p>
        <p className="price">¥{product.price.toLocaleString()}</p>
        <span className="discount-badge">{discountPercent}%OFF</span>
      </div>
      <p className="rating">
        {getStarRating(product.rating)}
        <span className="review-count">({product.reviewCount})</span>
      </p>
    </div>
  );
}

export default ProductInfo
📦 ProductCardでのProductInfo使用
import './styles.css'
import ProductImage from './ProductImage'
import ProductInfo from './ProductInfo'

const ProductCard = () => {
  return (
    <div className="product-card">
      <ProductImage />
      <ProductInfo />
    </div>
  )
}

export default ProductCard
Step 4

Propsでデータを管理しよう

コンポーネント分割ができました。次は、各コンポーネントにハードコーディングされているデータをProps(プロパティ)を使って外部から渡すように改善しましょう。 Propsを使うことで: - コンポーネントの再利用性が向上 - データの一元管理ができる - 異なる商品データで同じコンポーネントを使いまわせる 実装する内容: - App.jsxで商品データを定義 - ProductCard → ProductImage, ProductInfoへpropsでデータを渡す - 各コンポーネントのハードコーディングされたデータを削除

📦 App.jsxで商品データを定義し、複数のProductCardを表示
import ProductCard from './ProductCard'
import './styles.css'

const App = () => {
  // 4つの異なる商品データを定義
  const products = [
    {
      id: 1,
      name: "スマートウォッチ",
      brand: "TechGear",
      price: 12000,
      discountRate: 0.20,
      rating: 4,
      reviewCount: 128,
      imageUrl: "https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=400&h=533&fit=crop&crop=center",
      altText: "スマートウォッチの商品画像"
    },
    {
      id: 2,
      name: "ワイヤレスイヤホン",
      brand: "SoundMax",
      price: 8000,
      discountRate: 0.15,
      rating: 5,
      reviewCount: 95,
      imageUrl: "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=400&h=533&fit=crop&crop=center",
      altText: "ワイヤレスイヤホンの商品画像"
    },
    {
      id: 3,
      name: "デジタルカメラ",
      brand: "PhotoMax",
      price: 28000,
      discountRate: 0.15,
      rating: 5,
      reviewCount: 89,
      imageUrl: "https://images.unsplash.com/photo-1516035069371-29a1b244cc32?w=400&h=533&fit=crop&crop=center",
      altText: "デジタルカメラの商品画像"
    },
    {
      id: 4,
      name: "Bluetoothスピーカー",
      brand: "AudioWave",
      price: 15000,
      discountRate: 0.25,
      rating: 5,
      reviewCount: 203,
      imageUrl: "https://images.unsplash.com/photo-1608043152269-423dbba4e7e1?w=400&h=533&fit=crop&crop=center",
      altText: "Bluetoothスピーカーの商品画像"
    }
  ];

  return (
    <div className="app-container">
      <p className="category-path">All › 電化製品 › スマートデバイス</p>
      <div className="product-grid">
        {products.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  )
}

export default App
🔧 ProductCard.jsxをProps対応に修正
import './styles.css'
import ProductImage from './ProductImage'
import ProductInfo from './ProductInfo'

const ProductCard = ({ product }) => {
  return (
    <div className="product-card">
      <ProductImage product={product} />
      <ProductInfo product={product} />
    </div>
  )
}

export default ProductCard
🖼️ ProductImage.jsxをProps対応に修正
const ProductImage = ({ product }) => {
  return (
    <div className="product-image-container">
      <img 
        className="product-image"
        src={product.imageUrl}
        alt={product.altText}
      />
    </div>
  );
}

export default ProductImage
📋 ProductInfo.jsxをProps対応に修正
const ProductInfo = ({ product }) => {
  const getStarRating = (rating) => {
    return '★'.repeat(rating) + '☆'.repeat(5 - rating);
  }

  const discountPercent = Math.round(product.discountRate * 100);
  const discountedPrice = product.price - product.price * product.discountRate;

  return (
    <div className="product-info">
      <p className="brand-name">{product.brand}</p>
      <h1>{product.name}</h1>
      <div className="price-container">
        <p className="discount-price">¥{discountedPrice.toLocaleString()}</p>
        <p className="price">¥{product.price.toLocaleString()}</p>
        <span className="discount-badge">{discountPercent}%OFF</span>
      </div>
      <p className="rating">
        {getStarRating(product.rating)}
        <span className="review-count">({product.reviewCount})</span>
      </p>
    </div>
  );
}

export default ProductInfo

お疲れ様でした!次のレッスンへ進みましょう!

FILES
Loading Monaco Editor...