React

Reactで配列を表示する方法|map・filter・keyと表示されない原因

Reactで配列をリスト表示するmap・filterの書き方を初心者向けに解説します。安定したkey、オブジェクト配列、検索・空配列・非同期データ、state更新、mapしても表示されない原因までコードで確認できます。

この記事の目次
  1. Reactで配列を表示する基本
  2. mapのcallbackで受け取れる値
  3. object配列から複数propertyを表示する
  4. map結果をcomponentへ分ける
  5. Reactのkeyが必要な理由
  6. indexをkeyにしてよい場合
  7. 複数要素をFragmentで返す場合
  8. filterしてからmapで表示する
  9. 入力文字で配列を絞り込む
  10. 0件・loading・errorを分けて表示する
  11. API dataをmapする前に形を確認する
  12. stateの配列を追加・削除・更新する
  13. sortとreverseはcopyしてから使う
  14. nested配列を表示する
  15. mapしても表示されない原因
  16. callbackでJSXをreturnしていない
  17. component本体がJSXをreturnしていない
  18. 配列が空・null・undefined・objectになっている
  19. object全体をJSXへ書いている
  20. key warningが消えない
  21. 配列を変更したのに再renderされない
  22. 条件分岐でlistを隠している
  23. 実装チェックリスト
  24. まとめ

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として表示することはできません。画面へ出すnamepriceを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です。

スポンサーリンク