第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 を使って、登場人物ページのキャラクター選択機能を実装する。