Reactで配列を表示する基本は、map()で各dataをJSXへ変換することです。
const users = [
{ id: "u1", name: "佐藤" },
{ id: "u2", name: "鈴木" },
];
export default function UserList() {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
重要なのは、map()からJSXを返し、配列内で安定して一意になるkeyを最上位要素へ付けることです。
この記事では、文字列・object配列、filter()との組み合わせ、component化、state配列の追加・削除・更新、空配列やAPI data、mapしても表示されない原因まで順に解説します。
Reactで配列を表示する基本
JSXの波括弧内にはJavaScriptの式を書けます。map()は新しい配列を返す式なので、JSX内へ直接記述できます。
const animals = ["犬", "ゴリラ", "たぬき"];
export default function AnimalList() {
return (
<ul>
{animals.map((animal) => (
<li key={animal}>{animal}</li>
))}
</ul>
);
}
この例では文字列が重複しないため、文字列自体をkeyにできます。同じ値が複数あり得るdataでは一意にならないため、最初からIDを持つobject配列にします。
JSXへ式を埋め込む規則はReactのJSXにJavaScriptを書く方法で詳しく解説しています。
mapのcallbackで受け取れる値
map()のcallbackは、現在の要素、index、元配列を受け取れます。
items.map((item, index) => {
console.log(item, index);
return <li key={item.id}>{item.name}</li>;
});
indexは表示番号には使えますが、並べ替えや削除があるlistのkeyには通常使いません。
object配列から複数propertyを表示する
実務では文字列配列より、IDや名前を持つobject配列を扱うことが多くなります。
const products = [
{ id: 101, name: "キーボード", price: 8000, inStock: true },
{ id: 102, name: "マウス", price: 4500, inStock: false },
];
export default function ProductList() {
return (
<ul>
{products.map((product) => (
<li key={product.id}>
<strong>{product.name}</strong>
<span>{product.price.toLocaleString()}円</span>
{!product.inStock && <span>在庫切れ</span>}
</li>
))}
</ul>
);
}
{product}のようにobject全体をchildとして表示することはできません。画面へ出すnameやpriceをproperty単位で指定します。
map結果をcomponentへ分ける
1件分のJSXが長くなったらcomponentへ分けます。keyはmap()から直接返すcomponentへ付けます。
function ProductItem({ product }) {
return (
<li>
<strong>{product.name}</strong>
<span>{product.price.toLocaleString()}円</span>
</li>
);
}
export default function ProductList({ products }) {
return (
<ul>
{products.map((product) => (
<ProductItem key={product.id} product={product} />
))}
</ul>
);
}
ProductItem内部のliへkeyを付けても、親の配列からは識別できません。配列を作っている場所でProductItemへ付けます。
Reactのkeyが必要な理由
keyは、再render前後で「どのitemがどのitemに対応するか」をReactへ伝える識別子です。挿入、削除、並べ替えが起きても、同じkeyのcomponentを同じitemとして対応付けられます。
- keyは同じ配列の兄弟間で一意にする
- database IDなどdata由来の安定した値を使う
- renderのたびにkeyを変更しない
Math.random()やrender中のcrypto.randomUUID()を使わない- componentでIDも使う場合は別propとして渡す
// keyはReact内部用で、UserCardのpropsには含まれない
<UserCard key={user.id} userId={user.id} name={user.name} />
keyがない場合はconsole warningになりますが、通常はそれだけで初回のlistが完全に表示されなくなるわけではありません。問題は、更新時にitemのidentityを正しく保てないことです。
indexをkeyにしてよい場合
表示順が固定され、追加・削除・並べ替えがなく、item内部に入力値などのstateもない静的listならindexでも問題が表面化しにくい場合があります。
{steps.map((step, index) => (
<li key={index}>{step}</li>
))}
ただし、将来変更される可能性があるdataならIDを用意するほうが安全です。index keyの本質的な問題は、位置が変わったときに別itemへ同じidentityを割り当てることです。単なるperformance問題ではありません。
複数要素をFragmentで返す場合
short Fragmentの<>...</>にはkeyを付けられません。明示的なFragmentを使います。
import { Fragment } from "react";
export default function DefinitionList({ terms }) {
return (
<dl>
{terms.map((term) => (
<Fragment key={term.id}>
<dt>{term.name}</dt>
<dd>{term.description}</dd>
</Fragment>
))}
</dl>
);
}
HTMLの親子関係も守ります。ulまたはolの直接の子はliにし、補足要素はliの中へ入れます。
filterしてからmapで表示する
filter()は条件に合う要素だけを含む新しい配列を作ります。その結果をmap()でJSXへ変換できます。
const availableProducts = products.filter(
(product) => product.inStock
);
return (
<ul>
{availableProducts.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
1回だけ使う場合はchainで書くこともできます。
{products
.filter((product) => product.inStock)
.map((product) => (
<li key={product.id}>{product.name}</li>
))}
map・filter自体の返り値やreduceとの違いはJavaScriptのmap・filter・reduceで確認できます。
入力文字で配列を絞り込む
検索語だけをstateに持ち、絞り込み結果はrender中に計算します。
import { useState } from "react";
export default function SearchableList({ products }) {
const [query, setQuery] = useState("");
const normalizedQuery = query.trim().toLocaleLowerCase("ja");
const visibleProducts = products.filter((product) =>
product.name.toLocaleLowerCase("ja").includes(normalizedQuery)
);
return (
<>
<label>
商品を検索
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
</label>
<ul>
{visibleProducts.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</>
);
}
通常のfilter結果を別stateへ保存し、useEffectで同期する必要はありません。propsとstateから計算できる値はrender中に求めると、古い結果が一瞬表示される二重renderを避けられます。計算が実測で重い場合だけmemoizationを検討します。
0件・loading・errorを分けて表示する
空配列は正常な配列なのでmap()を呼べます。ただし要素が0件のため何も生成されません。0件messageを明示します。
function ProductList({ products, isLoading, error }) {
if (isLoading) return <p>読み込み中です…</p>;
if (error) return <p role="alert">取得に失敗しました。</p>;
if (products.length === 0) return <p>商品がありません。</p>;
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
{products.length && <List />}と書くと、0件時にnumberの0が画面へ出る場合があります。products.length > 0とbooleanへ変換してください。条件分岐の使い分けはReactの条件分岐に分離しています。
API dataをmapする前に形を確認する
「map is not a function」は、対象が配列ではないときに発生します。API response全体がobjectで、配列がitems property内にあるケースが典型です。
// response example
const data = {
items: [
{ id: 1, name: "キーボード" },
{ id: 2, name: "マウス" },
],
};
console.log(Array.isArray(data)); // false
console.log(Array.isArray(data.items)); // true
外部dataは配列か検証してからcomponentへ渡します。
const products = Array.isArray(data.items) ? data.items : [];
単にdata?.map()と書いても、dataがnullなら処理を省略できるだけで、dataがobjectならerrorになります。Network tabとconsole.log(data)で実際の形を確認してください。JSON取得の流れはReactでJSONデータを表示する方法も参照できます。
stateの配列を追加・削除・更新する
React state内の配列はread-onlyとして扱い、元配列を直接変更せず新しい配列をsetします。
| 操作 | 避ける | 使用する方法 |
|---|---|---|
| 追加 | push()、unshift() |
spread、concat() |
| 削除 | pop()、splice() |
filter() |
| 更新 | items[index] = value |
map() |
| 並べ替え | stateへ直接sort() |
copyしてからsort() |
// 追加
setProducts((current) => [
...current,
{ id: nextId, name: "モニター" },
]);
// 削除
setProducts((current) =>
current.filter((product) => product.id !== targetId)
);
// 1件更新
setProducts((current) =>
current.map((product) =>
product.id === targetId
? { ...product, name: "更新後の商品名" }
: product
)
);
push()で元配列を変更して同じreferenceをsetすると、期待した再renderにならない原因になります。useStateの基本はReact useStateの使い方で解説しています。
sortとreverseはcopyしてから使う
sort()とreverse()は元配列を変更します。spreadで配列をcopyしてから実行します。
const sortedProducts = [...products].sort(
(a, b) => a.price - b.price
);
ただしspreadはshallow copyです。配列内objectを変更する場合は、先ほどのmap()とobject spreadで対象objectも新しくします。
nested配列を表示する
categoryごとに商品listを持つ場合は、それぞれの階層でkeyを付けます。
{categories.map((category) => (
<section key={category.id}>
<h2>{category.name}</h2>
<ul>
{category.products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</section>
))}
categoryの兄弟間ではcategory ID、各category内の商品兄弟間ではproduct IDが一意なら問題ありません。別の配列同士で同じkeyが使われても構いません。
mapしても表示されない原因
callbackでJSXをreturnしていない
arrow functionで波括弧を使う場合はreturnが必要です。
// NG: block内で何もreturnしていない
items.map((item) => {
<li key={item.id}>{item.name}</li>;
});
// OK
items.map((item) => {
return <li key={item.id}>{item.name}</li>;
});
// OK: parenthesesでimplicit return
items.map((item) => (
<li key={item.id}>{item.name}</li>
));
component本体がJSXをreturnしていない
mapが正しくても、component側のreturnを忘れると表示できません。browser consoleとbuild errorを確認します。
配列が空・null・undefined・objectになっている
空配列はerrorになりませんが0件です。nullとundefinedにはmap()がありません。objectには配列用mapがないため、Array.isArray()とAPI responseのpropertyを確認します。
object全体をJSXへ書いている
<li>{item}</li>ではなく、<li>{item.name}</li>のように表示可能なpropertyを指定します。
key warningが消えない
keyをmapから直接返す最上位のJSXまたはcomponentへ付けます。Fragmentならshort syntaxではなく<Fragment key={item.id}>を使います。重複keyがないかも確認してください。
配列を変更したのに再renderされない
state配列へ直接push()やsplice()を使っている可能性があります。spread、filter、mapで新しい配列を作り、setterへ渡します。
条件分岐でlistを隠している
loading flag、検索条件、CSSのdisplay: none、親componentの早期returnを確認します。React DevToolsでpropsとstateの実値を見ると原因を絞れます。
実装チェックリスト
map()対象が実際に配列か確認した- callbackからJSXをreturnした
- object全体ではなく表示propertyを指定した
- map直下の最上位要素へkeyを付けた
- keyは兄弟間で一意かつ安定している
- render中にkeyを生成していない
- 0件・loading・errorを区別した
- filter結果を不要なEffectとstateへ複製していない
- state配列を直接変更していない
- nested listの各階層にkeyがある
- console warningとNetwork responseを確認した
まとめ
Reactで配列を表示するときは、map()でdataをJSXへ変換し、各itemの安定したIDをkeyへ設定します。条件で絞る場合はfilter()の結果をmapし、通常はrender中に計算すれば十分です。
表示されない場合は、keyだけを見るのではなく、callbackのreturn、配列の実際の形、object property、loading・0件条件、state mutationを順に確認してください。keyは初回表示のswitchではなく、更新前後でitemとcomponent stateを正しく対応付けるためのidentityです。