第8回

リストレンダリングとkey:配列データからUIを生成する

Reactでの配列のmap()によるリストレンダリングを解説。keyプロパティが必要な理由と、よくある間違いをFeatureCard実装を通じて学ぶ。

·18分で読める
たける
たける `map()` を使ったらブラウザのコンソールに「Each child in a list should have a unique "key" prop」って出てきました。エラーじゃないけど、無視していいですか?
りこ
りこ 無視しないで。Reactが正しく動作するために必要な情報。まず `key` が何かを理解してから追加して。

配列をUIに変換する

前回、features 配列を map() でレンダリングした。改めてその仕組みを整理する。

const features = [
  { id: 'dialogue', icon: '📖', title: '対話形式で学べる', description: '...' },
  { id: 'hands-on', icon: '🛠', title: '実際に動くものを作る', description: '...' },
  { id: 'deploy',   icon: '🚀', title: '公開まで完走する',  description: '...' },
]

// ✅ map() で各要素をJSXに変換
{features.map(f => (
  <FeatureCard key={f.id} icon={f.icon} title={f.title} description={f.description} />
))}

map() は配列の各要素を変換して新しい配列を返すJavaScriptの関数。ここでは「データのオブジェクト」を「JSX要素」に変換している。

たける
たける `key={f.id}` の `key` って何ですか? `FeatureCard` のpropsの型には書いてないですよね。
りこ
りこ `key` はpropsじゃなくてReact自身が使う特別な属性。Reactがリストの要素を追跡するための識別子。

keyが必要な理由

Reactはstate(状態)が変わると、前のUIと新しいUIを比較して「どこが変わったか」を計算し、変わった部分だけDOMを更新する。

リストの場合、この比較を正確にやるために 各要素がどれかを識別できるキーが必要

// key がない状態で、中間に要素を追加したとき:
// 変更前: [A, B, C]
// 変更後: [A, X, B, C]

// Reactは順番で比較するので、
// 2番目がB→Xに変わった、3番目がC→Bに変わった、4番目はCが新しく追加された
// と解釈してしまう。実際はXが追加されただけなのに。

// key があれば:
// Reactは id で追跡できるので、Xだけが新しく追加されたと正確に理解できる。

key はリスト内で**ユニーク(一意)**であれば何でもよい。同じコンポーネントでも、別のリストなら同じキーを使っても問題ない。

keyのよくある間違い

❌ インデックスをkeyにする

// 避けるべき書き方
{features.map((f, index) => (
  <FeatureCard key={index} {...f} />
))}

並び替えや中間への追加・削除が起きると、インデックスがずれてReactが正しく追跡できなくなる。パフォーマンスの低下や意図しない再レンダリングの原因になる。

たける
たける じゃあIDがないデータはどうすればいいですか?
りこ
りこ 「順番が変わらない・追加削除がない」と確実に言えるなら、インデックスでも実用上は問題ない。そうでなければ、データを作るときに `id` を持たせる習慣をつけて。

✅ ユニークなIDをkeyにする

// 元からIDがある場合
{characters.map(c => (
  <CharacterCard key={c.id} {...c} />
))}

// IDがない場合は、データを定義するときに追加する
const features = [
  { id: 'dialogue', icon: '📖', title: '...' },  // id を持たせる
  { id: 'hands-on', icon: '🛠', title: '...' },
]

filter・sortと組み合わせる

map() の前に filter()sort() を使うと、表示するデータを絞り込んだり並び替えたりできる。

const characters = [
  { id: 'riko',   name: '大沢りこ',  role: 'リーダー',   age: 39 },
  { id: 'takeru', name: '宮本たける', role: 'インターン', age: 26 },
  { id: 'natsumi',name: '林なつみ',  role: 'デザイナー',  age: 32 },
]

// 30歳以上だけ表示
{characters
  .filter(c => c.age >= 30)
  .map(c => <CharacterCard key={c.id} {...c} />)
}
たける
たける `.filter().map()` って繋げて書けるんですね。JavaScriptのメソッドチェーンか。
りこ
りこ 登場人物ページでスキルで絞り込む機能を作るとき、この書き方が活きる。第9回でstateを学んだら組み合わせてみて。

トップページの特徴セクション完成形

// src/components/Features.tsx
import { FeatureCard } from './FeatureCard'

const features = [
  {
    id: 'dialogue',
    icon: '📖',
    title: '対話形式で学べる',
    description: '登場人物との会話を通じてReactの概念を自然に習得できます。',
  },
  {
    id: 'hands-on',
    icon: '🛠',
    title: '実際に動くものを作る',
    description: '概念だけでなく、実際にWebサイトを完成させながら学びます。',
  },
  {
    id: 'deploy',
    icon: '🚀',
    title: '公開まで完走する',
    description: 'Cloudflare Pagesへのデプロイまで、一連の流れを体験します。',
  },
]

export const Features = () => {
  return (
    <section className="py-16 px-4 bg-slate-50">
      <div className="max-w-5xl mx-auto">
        <h2 className="text-2xl font-bold text-slate-900 text-center mb-10">
          このシリーズの特徴
        </h2>
        <div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
          {features.map(f => (
            <FeatureCard key={f.id} {...f} />
          ))}
        </div>
      </div>
    </section>
  )
}
たける
たける これでコンソールの警告も消えました! `key` をちゃんと付けるのと、データを配列で管理するのが大事なんですね。
りこ
りこ トップページの土台ができた。次は登場人物ページに入る。そこで `useState` が必要になる。
📁 第8回完了時点のファイル構成・完成コード

フォルダ構成

src/
├── components/
│   ├── Header.tsx
│   ├── Footer.tsx
│   ├── FeatureCard.tsx   ← 第6回で作成(変更なし)
│   └── Features.tsx      ← 今回追加
└── App.tsx               ← Features を追加

FeatureCard.tsx

type FeatureCardProps = {
  icon: string
  title: string
  description: string
}

export const FeatureCard = ({ icon, title, description }: FeatureCardProps) => {
  return (
    <div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-6">
      <span className="text-3xl mb-4 block">{icon}</span>
      <h3 className="text-lg font-bold text-slate-900 mb-2">{title}</h3>
      <p className="text-slate-600 text-sm leading-7">{description}</p>
    </div>
  )
}

Features.tsx

import { FeatureCard } from './FeatureCard'

const features = [
  {
    id: 'dialogue',
    icon: '📖',
    title: '対話形式で学べる',
    description: '登場人物との会話を通じてReactの概念を自然に習得できます。',
  },
  {
    id: 'hands-on',
    icon: '🛠',
    title: '実際に動くものを作る',
    description: '概念だけでなく、実際にWebサイトを完成させながら学びます。',
  },
  {
    id: 'deploy',
    icon: '🚀',
    title: '公開まで完走する',
    description: 'Cloudflare Pagesへのデプロイまで、一連の流れを体験します。',
  },
]

export const Features = () => {
  return (
    <section className="py-16 px-4 bg-slate-50">
      <div className="max-w-5xl mx-auto">
        <h2 className="text-2xl font-bold text-slate-900 text-center mb-10">
          このシリーズの特徴
        </h2>
        <div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
          {features.map(f => (
            <FeatureCard key={f.id} {...f} />
          ))}
        </div>
      </div>
    </section>
  )
}

App.tsx

import { Header } from './components/Header'
import { Footer } from './components/Footer'
import { Features } from './components/Features'

function App() {
  return (
    <div className="min-h-screen flex flex-col">
      <Header />
      <main className="flex-1">
        <section className="max-w-5xl mx-auto px-4 sm:px-8 py-16 text-center">
          <h1 className="text-4xl font-bold text-slate-900 mb-4">
            Reactを実践的に学ぼう
          </h1>
          <p className="text-slate-600 text-lg">
            対話形式でReactの基礎から公開まで体験できる入門サイト
          </p>
        </section>
        <Features />
      </main>
      <Footer />
    </div>
  )
}

export default App

まとめ

  • array.map(item => <Component key={item.id} {...item} />) が配列レンダリングの基本形
  • key はReactが各要素を追跡するための識別子。リストの中でユニークな値を使う
  • インデックスをkeyにするのは避ける(順番変更・追加削除があると壊れる)
  • filter()map() のチェーンで絞り込んでからレンダリングできる

次の第9回では useState を使って、登場人物ページのキャラクター選択機能を実装する。