デザインについての学習メモブログ

React入門 #06 – propsでデータを受け渡そう

記事内に広告が含まれています。

React入門 #06 – propsでデータを受け渡そう

前回の記事でコンポーネントの基本を学びました。

この記事では、propsについてさらに深く掘り下げ、実践的なデータの受け渡し方法をマスターしていきます。

propsを理解することで、柔軟で再利用可能なコンポーネントを作れるようになります。

propsとは?

propsはReactにおける「データの受け渡し」の基本概念です。

コンポーネント間でどのようにデータをやり取りするのか、その仕組みと特徴を理解しましょう。

Reactで様々なコンポーネントを組み合わせてアプリケーションを作る際、必ず必要になるのがコンポーネント間のデータのやり取りです。

propsはその中心的な役割を担う仕組みです。

propsの概念

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

関数の引数のようなものと考えると分かりやすいでしょう。

イメージ図:

JSX
親コンポーネント

propsデータを渡す

子コンポーネント

propsの特徴

1. 読み取り専用

  • 子コンポーネント内でpropsを変更できない
  • データは一方向にのみ流れる(単方向データフロー)

2. 任意のデータ型を渡せる

  • 文字列、数値、真偽値
  • 配列、オブジェクト
  • 関数
  • JSX要素

3. 動的に変化する

  • 親コンポーネントでpropsが変わると、子コンポーネントも再レンダリングされる

基本的なpropsの使い方

まずは最もシンプルなpropsの使い方から始めましょう。

文字列、数値、真偽値といった基本的なデータ型を渡す方法を学びます。

文字列を渡す

JavaScript版:

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

// 親コンポーネント
function App() {
  return (
    <div>
      <Greeting name="太郎" />
      <Greeting name="花子" />
    </div>
  )
}

TypeScript版:

TSX
interface GreetingProps {
  name: string
}

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

function App(): JSX.Element {
  return (
    <div>
      <Greeting name="太郎" />
      <Greeting name="花子" />
    </div>
  )
}

5行目の{ name }: GreetingProps分割代入により、インラフェースの特定のプロパティの型と値を取得(ここではstring型のname変数へ値代入されたイメージ)

数値と真偽値を渡す

文字列以外のデータは{}で囲みます。

JavaScript版:

JSX
function ProductCard({ name, price, inStock }) {
  return (
    <div className="product">
      <h3>{name}</h3>
      <p>¥{price.toLocaleString()}</p>
      {inStock ? (
        <span className="badge-success">在庫あり</span>
      ) : (
        <span className="badge-danger">在庫切れ</span>
      )}
    </div>
  )
}

// 使い方
function App() {
  return (
    <div>
      <ProductCard name="ノートPC" price={120000} inStock={true} />
      <ProductCard name="マウス" price={3000} inStock={false} />
    </div>
  )
}

TypeScript版:

TSX
interface ProductCardProps {
  name: string
  price: number
  inStock: boolean
}

function ProductCard({ name, price, inStock }: ProductCardProps): JSX.Element {
  return (
    <div className="product">
      <h3>{name}</h3>
      <p>¥{price.toLocaleString()}</p>
      {inStock ? (
        <span className="badge-success">在庫あり</span>
      ) : (
        <span className="badge-danger">在庫切れ</span>
      )}
    </div>
  )
}

複雑なデータ型を渡す

実際のアプリケーション開発では、配列オブジェクトなど複雑なデータ構造を扱うことが多くなります。

ここでは、それらをpropsとして渡す方法を学びます。

配列を渡す

JavaScript版:

JSX
function TagList({ tags }) {
  return (
    <div className="tags">
      {tags.map((tag, index) => (
        <span key={index} className="tag">
          {tag}
        </span>
      ))}
    </div>
  )
}

// 使い方
function App() {
  const articleTags = ['React', 'JavaScript', '入門']
  
  return <TagList tags={articleTags} />
}

⚠️ keyにindexを指定する時

5行目のように、keyにindexを指定するのはアンチパターンです。

#4でも触れましたがトラブルの原因になる事があります。

ただし、今回は、TagListコンポーネントの中で、要素の「追加」や「削除」、「順番の変更」の処理が入っていないので、トラブルが起こることはないです。唯一indexを指定して良いパターンです。

keyにindexを指定する時は、注意しましょう。

TypeScript版:

TSX
interface TagListProps {
  tags: string[]
}

function TagList({ tags }: TagListProps): JSX.Element {
  return (
    <div className="tags">
      {tags.map((tag, index) => (
        <span key={index} className="tag">
          {tag}
        </span>
      ))}
    </div>
  )
}

オブジェクトを渡す

JavaScript版:

JSX
function UserProfile({ user }) {
  return (
    <div className="profile">
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      <p>年齢: {user.age}</p>
      <p>職業: {user.job}</p>
      <p>メール: {user.email}</p>
    </div>
  )
}

// 使い方
function App() {
  const userData = {
    name: '山田太郎',
    age: 30,
    job: 'エンジニア',
    email: 'yamada@example.com',
    avatar: 'https://via.placeholder.com/150'
  }
  
  return <UserProfile user={userData} />
}

TypeScript版:

TSX
interface User {
  name: string
  age: number
  job: string
  email: string
  avatar: string
}

interface UserProfileProps {
  user: User
}

function UserProfile({ user }: UserProfileProps): JSX.Element {
  return (
    <div className="profile">
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      <p>年齢: {user.age}</p>
      <p>職業: {user.job}</p>
      <p>メール: {user.email}</p>
    </div>
  )
}

オブジェクトの配列を渡す

JavaScript版:

JSX
function UserList({ users }) {
  return (
    <div className="user-list">
      {users.map(user => (
        <div key={user.id} className="user-card">
          <h3>{user.name}</h3>
          <p>{user.email}</p>
        </div>
      ))}
    </div>
  )
}

// 使い方
function App() {
  const users = [
    { id: 1, name: '山田太郎', email: 'yamada@example.com' },
    { id: 2, name: '佐藤花子', email: 'sato@example.com' },
    { id: 3, name: '鈴木次郎', email: 'suzuki@example.com' }
  ]
  
  return <UserList users={users} />
}

TypeScript版:

TSX
interface User {
  id: number
  name: string
  email: string
}

interface UserListProps {
  users: User[]
}

function UserList({ users }: UserListProps): JSX.Element {
  return (
    <div className="user-list">
      {users.map(user => (
        <div key={user.id} className="user-card">
          <h3>{user.name}</h3>
          <p>{user.email}</p>
        </div>
      ))}
    </div>
  )
}

関数をpropsとして渡す

Reactの強力な機能の一つが、関数propsとして渡せることです。

これにより、子コンポーネントから親コンポーネントへイベントを通知できます。

イベントハンドラを渡す

親コンポーネントで定義した関数を、子コンポーネントに渡すことができます。

JavaScript版:

JSX
// 子コンポーネント
function Button({ text, onClick }) {
  return (
    <button onClick={onClick}>
      {text}
    </button>
  )
}

// 親コンポーネント
function App() {
  const handleClick = () => {
    alert('ボタンがクリックされました!')
  }
  
  return (
    <div>
      <Button text="クリック" onClick={handleClick} />
    </div>
  )
}

TypeScript版:

TSX
interface ButtonProps {
  text: string
  onClick: () => void
}

function Button({ text, onClick }: ButtonProps): JSX.Element {
  return (
    <button onClick={onClick}>
      {text}
    </button>
  )
}

function App(): JSX.Element {
  const handleClick = (): void => {
    alert('ボタンがクリックされました!')
  }
  
  return (
    <div>
      <Button text="クリック" onClick={handleClick} />
    </div>
  )
}

引数付きの関数を渡す

JavaScript版:

JSX
function TodoItem({ todo, onDelete }) {
  return (
    <div className="todo-item">
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>
        削除
      </button>
    </div>
  )
}

function TodoList() {
  const todos = [
    { id: 1, text: '買い物に行く' },
    { id: 2, text: '洗濯をする' },
    { id: 3, text: '本を読む' }
  ]
  
  const handleDelete = (id) => {
    console.log(`Todo ${id} を削除`)
  }
  
  return (
    <div>
      {todos.map(todo => (
        <TodoItem 
          key={todo.id} 
          todo={todo} 
          onDelete={handleDelete}
        />
      ))}
    </div>
  )
}

TypeScript版:

TSX
interface Todo {
  id: number
  text: string
}

interface TodoItemProps {
  todo: Todo
  onDelete: (id: number) => void
}

function TodoItem({ todo, onDelete }: TodoItemProps): JSX.Element {
  return (
    <div className="todo-item">
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>
        削除
      </button>
    </div>
  )
}

interface TodoListProps {
  // 必要に応じてpropsを定義
}

function TodoList(): JSX.Element {
  const todos: Todo[] = [
    { id: 1, text: '買い物に行く' },
    { id: 2, text: '洗濯をする' },
    { id: 3, text: '本を読む' }
  ]
  
  const handleDelete = (id: number): void => {
    console.log(`Todo ${id} を削除`)
  }
  
  return (
    <div>
      {todos.map(todo => (
        <TodoItem 
          key={todo.id} 
          todo={todo} 
          onDelete={handleDelete}
        />
      ))}
    </div>
  )
}

8行目にonDelete: (id: number) => voidと引数の型をnumberに指定しているので、TodoItemのonDeleteに渡すイベントハンドラの型も確実にチェックしてくれます。

デフォルト値と省略可能なprops

すべてのpropsを必須にすると、コンポーネントの使い勝手が悪くなります。

デフォルト値を設定することで、より柔軟なコンポーネント設計が可能になります。

デフォルト値の設定

JavaScript版:

JSX
function Button({ 
  text = 'クリック',
  color = 'blue',
  size = 'medium'
}) {
  return (
    <button 
      style={{ 
        backgroundColor: color,
        padding: size === 'large' ? '12px 24px' : '8px 16px'
      }}
    >
      {text}
    </button>
  )
}

// 使い方
function App() {
  return (
    <div>
      <Button />  {/* すべてデフォルト値 */}
      <Button text="送信" />  {/* textのみ指定 その他はその他はデフォルト */}
      <Button text="削除" color="red" size="large" />
    </div>
  )
}

TypeScript版:

TSX
interface ButtonProps {
  text?: string
  color?: string
  size?: 'small' | 'medium' | 'large'
}

function Button({ 
  text = 'クリック',
  color = 'blue',
  size = 'medium'
}: ButtonProps): JSX.Element {
  const padding = {
    small: '6px 12px',
    medium: '8px 16px',
    large: '12px 24px'
  }
  
  return (
    <button 
      style={{ 
        backgroundColor: color,
        padding: padding[size]
      }}
    >
      {text}
    </button>
  )
}

必須と省略可能を明確にする

TypeScriptでは、どのpropsが必須でどれが省略可能かを型レベルで明示できます。

?“を使って省略可能なプロパティを示すことで、コンポーネントの使い方が一目で分かり、誤った使用を防げます。

TypeScript版:

TSX
interface UserCardProps {
  // 必須
  name: string
  email: string
  
  // 省略可能(?を付ける)
  age?: number
  avatar?: string
  job?: string
}

function UserCard({ 
  name, 
  email, 
  age, 
  avatar = 'https://example.com/noimage.png',
  job 
}: UserCardProps): JSX.Element {
  return (
    <div className="user-card">
      <img src={avatar} alt={name} />
      <h2>{name}</h2>
      <p>{email}</p>
      {age && <p>年齢: {age}</p>}
      {job && <p>{job}</p>}
    </div>
  )
}

// 使い方
<UserCard name="山田太郎" email="yamada@example.com" />
<UserCard 
  name="佐藤花子" 
  email="sato@example.com" 
  age={25}
  job="Webデザイナーです"
/>

childrenプロパティの活用

childrenは特別なpropsで、コンポーネントタグの間に書かれた内容を受け取ります。

これを使うことで、より柔軟で再利用性の高いコンポーネントが作れます。

基本的なchildren

JavaScript版:

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

// 使い方
function App() {
  return (
    <Card>
      <h2>カードのタイトル</h2>
      <p>カードの内容です</p>
    </Card>
  )
}

TypeScript版:

TSX
interface CardProps {
  children: React.ReactNode
}

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

childrenと他のpropsの組み合わせ

childrenは他のpropsと一緒に使うことができます。

これにより、コンテンツ部分は柔軟に受け取りつつ、タイトルや動作を制御するpropsを別に定義できる、より実用的なコンポーネントが作れます。

JavaScript版:

JSX
function Modal({ title, isOpen, onClose, children }) {
  if (!isOpen) return null
  
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h2>{title}</h2>
          <button onClick={onClose}>×</button>
        </div>
        <div className="modal-body">
          {children}
        </div>
      </div>
    </div>
  )
}

// 使い方
function App() {
  const [isOpen, setIsOpen] = useState(false)
  
  return (
    <div>
      <button onClick={() => setIsOpen(true)}>モーダルを開く</button>
      
      <Modal 
        title="確認" 
        isOpen={isOpen} 
        onClose={() => setIsOpen(false)}
      >
        <p>本当に削除しますか?</p>
        <button>削除</button>
        <button>キャンセル</button>
      </Modal>
    </div>
  )
}

TypeScript版:

TSX
interface ModalProps {
  title: string
  isOpen: boolean
  onClose: () => void
  children: React.ReactNode
}

function Modal({ title, isOpen, onClose, children }: ModalProps): JSX.Element | null {
  if (!isOpen) return null
  
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h2>{title}</h2>
          <button onClick={onClose}>×</button>
        </div>
        <div className="modal-body">
          {children}
        </div>
      </div>
    </div>
  )
}

childrenという名前は変更できません。

これはReactの仕様で、コンポーネントタグの間に書かれた内容は必ずchildrenというプロパティ名で渡されます。

ただし、受け取った後に別の変数名に代入することは可能です。

複数のchildren(名前付きスロット)

childrenは1つしか使えませんが、複数の領域に異なるコンテンツを配置したい場合はどうすればいいでしょうか?

答えは、それぞれの領域を明示的なpropsとして定義することです。

例えば、Webサイトのレイアウトを考えてみましょう。

ヘッダー、サイドバー、メインコンテンツ、フッターなど、複数の異なる領域があります。

これらを別々のpropsとして受け取ることで、柔軟で再利用可能なレイアウトコンポーネントが作れます。

JavaScript版:

JSX
function Layout({ header, sidebar, children, footer }) {
  return (
    <div className="layout">
      <header className="header">{header}</header>
      <div className="main-container">
        <aside className="sidebar">{sidebar}</aside>
        <main className="content">{children}</main>
      </div>
      <footer className="footer">{footer}</footer>
    </div>
  )
}

// 使い方
function App() {
  return (
    <Layout
      header={<h1>マイサイト</h1>}
      sidebar={
        <nav>
          <a href="/">ホーム</a>
          <a href="/about">概要</a>
        </nav>
      }
      footer={<p>© 2025 マイサイト</p>}
    >
      <h2>メインコンテンツ</h2>
      <p>ここに本文が入ります</p>
    </Layout>
  )
}

TypeScript版:

TSX
interface LayoutProps {
  header: React.ReactNode
  sidebar: React.ReactNode
  children: React.ReactNode
  footer: React.ReactNode
}

function Layout({ header, sidebar, children, footer }: LayoutProps): JSX.Element {
  return (
    <div className="layout">
      <header className="header">{header}</header>
      <div className="main-container">
        <aside className="sidebar">{sidebar}</aside>
        <main className="content">{children}</main>
      </div>
      <footer className="footer">{footer}</footer>
    </div>
  )
}

propsのスプレッド構文

スプレッド構文を使うと、複数のpropsを効率的に渡すことができます。

特にHTML要素の属性を転送する際に便利です。

複数のpropsをまとめて渡す

HTMLの<input>要素のように、多くの属性(type、placeholder、required、disabledなど)を持つコンポーネントをラップする場合、すべての属性を個別に定義するのは大変です。

スプレッド構文{...props}を使えば、受け取ったpropsをそのまま子要素に渡すことができ、コードを簡潔に保てます。

JavaScript版:

JSX
function Input(props) {
  return <input {...props} />
}

// 使い方
function Form() {
  return (
    <div>
      <Input type="text" placeholder="名前" required />
      <Input type="email" placeholder="メール" required />
      <Input type="password" placeholder="パスワード" />
    </div>
  )
}

一部を上書きする

スプレッド構文を使いつつ、特定のpropsだけは独自の処理をしたい場合があります。

例えば、ボタンコンポーネントでvariantというカスタムpropを受け取り、それ以外のHTML属性はそのまま渡したいケースです。

分割代入とスプレッド構文を組み合わせることで、柔軟なコンポーネント設計が可能になります。

JavaScript版:

JSX
function Button({ variant = 'primary', ...rest }) {
  const className = `btn btn-${variant}`
  
  return <button className={className} {...rest} />
}

// 使い方
<Button variant="primary" onClick={handleClick}>
  クリック
</Button>
<Button variant="danger" type="submit" disabled>
  送信
</Button>

TypeScript版:

TSX
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'danger'
}

function Button({ variant = 'primary', ...rest }: ButtonProps): JSX.Element {
  const className = `btn btn-${variant}`
  
  return <button className={className} {...rest} />
}

ButtonPropsの型定義に…restがなくても・・・?

1行目のButtonPropsの型定義にrestについて一切書いていないのに

5行目で{ variant = ‘primary’, …rest }: ButtonProps

と引数の定義ができるのはextendsで型を継承しているからです。それに加えて分割代入で各プロパティを取り出しています。

React.ButtonHTMLAttributesとは?

React.ButtonHTMLAttributesは、ReactがTypeScript向けに用意している、HTML <button>要素が持つすべての属性の型定義です。

propsのバリデーション(TypeScript)

TypeScriptを使うと、propsの型を厳密に定義でき、開発時のエラーを防げます。

より安全で保守性の高いコードを書くための型定義テクニックを学びましょう。

より厳密な型定義

TypeScriptの強力な機能を使えば、propsに渡せる値をさらに細かく制限できます。

ユニオン型で「この3つの値だけ」と指定したり、Enumで定数を定義したり、オブジェクトの形状を厳密に決めたり。

こうした型定義により、間違った値を渡すとコンパイル時にエラーになるため、バグを未然に防げます。

TypeScript版:

TSX
// ユニオン型
interface ButtonProps {
  size: 'small' | 'medium' | 'large'
  variant: 'primary' | 'secondary' | 'danger'
}

// 列挙型(Enum)
enum UserRole {
  Admin = 'admin',
  User = 'user',
  Guest = 'guest'
}

interface UserBadgeProps {
  role: UserRole
}

function UserBadge({ role }: UserBadgeProps): JSX.Element {
  return <span className={`badge-${role}`}>{role}</span>
}

// 使い方
<UserBadge role={UserRole.Admin} />



// オブジェクトの形状を厳密に
interface Address {
  street: string
  city: string
  zipCode: string
}

interface UserWithAddressProps {
  name: string
  address: Address
}



// 配列の型を厳密に
interface TodoListProps {
  todos: Array<{
    id: number
    text: string
    completed: boolean
  }>
}

条件付きの型

TypeScriptの高度な機能として、「あるpropsの値によって、他のpropsが必須になったり不要になったりする」という条件付きの型定義ができます。

例えば、variant'link'の時はhrefが必須でonClickは不要、variant'button'の時は逆、といった複雑なルールを型レベルで表現できます。

これにより、誤った組み合わせでコンポーネントを使うことを防げます。

TypeScript版:

TSX
// variantによってonClickが必須かどうか変わる
type ButtonProps = 
  | {
      variant: 'link'
      href: string
      onClick?: never
    }
  | {
      variant: 'button'
      href?: never
      onClick: () => void
    }

function SmartButton(props: ButtonProps): JSX.Element {
  if (props.variant === 'link') {
    return <a href={props.href}>リンク</a>
  }
  return <button onClick={props.onClick}>ボタン</button>
}

3行目から12行目では、ユニオン型の2つの値がそれぞれ違う構造の組み合わせをしたオブジェクトとして指定しています。

これにより、どちらかの組み合わせのオブジェクトしか渡せないように限定しています。

よくあるpropsの間違い

propsを扱う際によくある間違いとその解決方法を知っておくことで、バグを未然に防ぎ、パフォーマンスの良いアプリケーションを作れます。

間違い1: propsを直接変更する

❌ 悪い例:

JSX
function Counter({ count }) {
  count = count + 1  // エラー!propsは読み取り専用
  return <div>{count}</div>
}

✅ 良い例:

JSX
function Counter({ count, onIncrement }) {
  return (
    <div>
      <span>{count}</span>
      <button onClick={onIncrement}>+1</button>
    </div>
  )
}

間違い2: オブジェクトや配列をそのまま比較

Reactは、propsが変更されたかを判断するために参照(メモリ上のアドレス)を比較します。

JSXの中で直接{ name: '太郎' }のようにオブジェクトを書くと、レンダリングのたびに新しいオブジェクトが作成され、中身が同じでも「別のオブジェクト」と判断されます。

その結果、不要な再レンダリングが発生し、パフォーマンスが低下する原因になります。

❌ 悪い例:

JSX
// 毎回新しいオブジェクトを作成 → 不要な再レンダリング
function App() {
  return <UserCard user={{ name: '太郎', age: 25 }} />
}

✅ 良い例:

JSX
function App() {
  // コンポーネント外またはuseStateで管理
  const user = { name: '太郎', age: 25 }
  return <UserCard user={user} />
}

間違い3: インラインで関数を定義

オブジェクトと同様に、関数も参照で比較されます。

JSXの中でonClick={() => ...}のようにインライン関数を書くと、レンダリングのたびに新しい関数が作成されます。

これもpropsの変更と判断され、子コンポーネントの不要な再レンダリングを引き起こします。

特にリストの各要素でインライン関数を使うと、パフォーマンスへの影響が大きくなります。

❌ 悪い例:

JSX
function App() {
  return (
    <div>
      {/* 毎回新しい関数が作成される */}
      <Button onClick={() => console.log('クリック')} />
    </div>
  )
}

✅ 良い例:

JSX
function App() {
  const handleClick = () => {
    console.log('クリック')
  }
  
  return <Button onClick={handleClick} />
}

// または useCallback を使う(後の記事で解説)

実践:ショッピングカート

ここまで学んだpropsの知識を総動員して、実際のショッピングカート機能を作ってみましょう。

複数のコンポーネント間でのデータのやり取りを実践的に学べます。

JavaScript版:

JSX
// CartItem.jsx
function CartItem({ item, onUpdateQuantity, onRemove }) {
  return (
    <div className="cart-item">
      <img src={item.image} alt={item.name} />
      <div className="item-details">
        <h3>{item.name}</h3>
        <p className="price">¥{item.price.toLocaleString()}</p>
      </div>
      <div className="quantity-controls">
        <button onClick={() => onUpdateQuantity(item.id, item.quantity - 1)}>
          -
        </button>
        <span>{item.quantity}</span>
        <button onClick={() => onUpdateQuantity(item.id, item.quantity + 1)}>
          +
        </button>
      </div>
      <button className="remove-btn" onClick={() => onRemove(item.id)}>
        削除
      </button>
    </div>
  )
}






// ShoppingCart.jsx
function ShoppingCart({ items, onUpdateQuantity, onRemove }) {
  const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  
  return (
    <div className="shopping-cart">
      <h2>ショッピングカート</h2>
      {items.length === 0 ? (
        <p>カートは空です</p>
      ) : (
        <>
          {items.map(item => (
            <CartItem
              key={item.id}
              item={item}
              onUpdateQuantity={onUpdateQuantity}
              onRemove={onRemove}
            />
          ))}
          <div className="cart-total">
            <h3>合計: ¥{total.toLocaleString()}</h3>
            <button className="checkout-btn">購入する</button>
          </div>
        </>
      )}
    </div>
  )
}







// App.jsx
import { useState } from 'react'

function App() {
  const [cartItems, setCartItems] = useState([
    { id: 1, name: 'ノートPC', price: 120000, quantity: 1, image: 'laptop.jpg' },
    { id: 2, name: 'マウス', price: 3000, quantity: 2, image: 'mouse.jpg' }
  ])
  
  const handleUpdateQuantity = (id, newQuantity) => {
    if (newQuantity < 1) return
    setCartItems(cartItems.map(item =>
      item.id === id ? { ...item, quantity: newQuantity } : item
    ))
  }
  
  const handleRemove = (id) => {
    setCartItems(cartItems.filter(item => item.id !== id))
  }
  
  return (
    <ShoppingCart
      items={cartItems}
      onUpdateQuantity={handleUpdateQuantity}
      onRemove={handleRemove}
    />
  )
}

TypeScript版:

TSX
// types.ts
export interface CartItem {
  id: number
  name: string
  price: number
  quantity: number
  image: string
}






// CartItem.tsx
import { type JSX } from 'react'
import { type CartItem as CartItemType } from './types'

interface CartItemProps {
  item: CartItemType
  onUpdateQuantity: (id: number, quantity: number) => void
  onRemove: (id: number) => void
}

function CartItem({ item, onUpdateQuantity, onRemove }: CartItemProps): JSX.Element {
  return (
    <div className="cart-item">
      <img src={item.image} alt={item.name} />
      <div className="item-details">
        <h3>{item.name}</h3>
        <p className="price">¥{item.price.toLocaleString()}</p>
      </div>
      <div className="quantity-controls">
        <button onClick={() => onUpdateQuantity(item.id, item.quantity - 1)}>
          -
        </button>
        <span>{item.quantity}</span>
        <button onClick={() => onUpdateQuantity(item.id, item.quantity + 1)}>
          +
        </button>
      </div>
      <button className="remove-btn" onClick={() => onRemove(item.id)}>
        削除
      </button>
    </div>
  )
}

export default CartItem







// ShoppingCart.tsx
import { type JSX } from 'react'
import { type CartItem as CartItemType } from './types'
import CartItem from './CartItem'

interface ShoppingCartProps {
  items: CartItemType[]
  onUpdateQuantity: (id: number, quantity: number) => void
  onRemove: (id: number) => void
}

function ShoppingCart({ 
  items, 
  onUpdateQuantity, 
  onRemove 
}: ShoppingCartProps): JSX.Element {
  const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  
  return (
    <div className="shopping-cart">
      <h2>ショッピングカート</h2>
      {items.length === 0 ? (
        <p>カートは空です</p>
      ) : (
        <>
          {items.map(item => (
            <CartItem
              key={item.id}
              item={item}
              onUpdateQuantity={onUpdateQuantity}
              onRemove={onRemove}
            />
          ))}
          <div className="cart-total">
            <h3>合計: ¥{total.toLocaleString()}</h3>
            <button className="checkout-btn">購入する</button>
          </div>
        </>
      )}
    </div>
  )
}

export default ShoppingCart




// App.jsx
import ShoppingCart from './ShoppingCart'
import { useState } from 'react'

function App() {
  const [cartItems, setCartItems] = useState([
    {
      id: 1,
      name: 'ノートPC',
      price: 120000,
      quantity: 1,
      image: 'laptop.jpg',
    },
    { id: 2, name: 'マウス', price: 3000, quantity: 2, image: 'mouse.jpg' },
  ])

  const handleUpdateQuantity = (id: number, newQuantity: number) => {
    if (newQuantity < 1) return
    setCartItems(
      cartItems.map((item) =>
        item.id === id ? { ...item, quantity: newQuantity } : item
      )
    )
  }

  const handleRemove = (id: number) => {
    setCartItems(cartItems.filter((item) => item.id !== id))
  }

  return (
    <ShoppingCart
      items={cartItems}
      onUpdateQuantity={handleUpdateQuantity}
      onRemove={handleRemove}
    />
  )
}

export default App

TypeScript版で注目すべきポイント

このサンプルコードには、これまで学んだpropsのベストプラクティスが詰まっています:

  1. 型の再利用CartItem型を別ファイルで定義し、複数コンポーネントで共有
  2. 型の明確なimportimport { type ... }で型であることを明示
  3. 関数型の定義onUpdateQuantityonRemoveのような関数propsの型を厳密に定義
  4. コンポーネントの責務分離CartItem(個別アイテム)とShoppingCart(リスト全体)を分離
  5. 状態管理とprops – 親コンポーネント(App)で状態を管理し、子に関数を渡して更新

まとめ

この記事では、propsの実践的な使い方を詳しく学びました。

重要なポイント:

  • propsは親から子へデータを渡す仕組み
  • propsは読み取り専用(変更不可)
  • 文字列、数値、配列、オブジェクト、関数など何でも渡せる
  • デフォルト値で柔軟なコンポーネント設計
  • childrenで再利用性の高いコンポーネント
  • TypeScriptで型安全にpropsを扱う

ベストプラクティス:

  • propsは直接変更しない
  • 分割代入で読みやすく
  • TypeScriptで型を明示
  • デフォルト値を活用
  • 必要最小限のpropsに抑える

次のステップ: 次回は、useStateを使った状態管理について学びます。