Reactで配列内の配列を取り出す

React勉強中のサラリーマンです。
今日はReactのキモであるStateがどうのこうの…の以前に、ネストされた配列の処理について理解に苦労したので、整理するために書いていきます。

例として使う配列

export const recipes = [{
  id: 'greek-salad',
  name: 'Greek Salad',
  ingredients: ['tomatoes', 'cucumber', 'onion', 'olives', 'feta']
}, {
  id: 'hawaiian-pizza',
  name: 'Hawaiian Pizza',
  ingredients: ['pizza crust', 'pizza sauce', 'mozzarella', 'ham', 'pineapple']
}, {
  id: 'hummus',
  name: 'Hummus',
  ingredients: ['chickpeas', 'olive oil', 'garlic cloves', 'lemon', 'tahini']
}];

この配列は、簡単に言えば「料理の配列があって、それぞれの料理には材料の配列が含まれる」というものです。

やりたいこと

上記のコードのままでは、「リスト」として書き出すことができません。
読みにくいので、最終的には以下のようにリストにしたいです。

イメージ画像です

考え方

大まかな考え方は以下。

  1. トップレベルの配列(全料理)から、「それぞれの料理」を取り出す

  2. 各料理に含まれるボトムレベルの配列から、「それぞれの材料」を取り出す

  3. 材料がリストとして並ぶ

1. トップレベルの配列から、それぞれの料理を取り出す

export default function RecipeList() {
  return (
    <div>
      <h1>Recipes</h1>
      {recipes.map((recipe) => (
        <div key={recipe.id}>
          <h2>{recipe.name}</h2>
        </div>
      ))}
    </div>
  );
}

このコードがやっていることは以下。

  • export default function RecipeList()
    RecipeListというコンポーネントを宣言する

  • <h1>Recipes</h1>
    Recipesというトップレベルの見出しを描画する

  • {recipes.map((recipe) => (
    recipes(全料理)からrecipe(ひとつの料理)を取り出して並べる(map)

  • <div key={recipe.id}> <h2>{recipe.name}</h2>
    recipeのid番号を付与した要素として、各recipeの名前を表示する

2-1. それぞれの材料を取り出す

「料理リストの中の材料リスト」のように入れ子構造になっていることをプログラミング用語で「ネストされている」といいます。
このようにネストされた配列を操作するにはいくつかのアプローチがあります。
例えば、呼び出すコード自体をネストしてしまうことです。

{recipes.map(recipe =>
        <div key={recipe.id}>
          <h2>{recipe.name}</h2>
          <ul>
            {recipe.ingredients.map(ingredient =>
              <li key={ingredient}>
                {ingredient}
              </li>
            )}
          </ul>

この書き方を自然言語混じりで言うと、
「料理"recipe"の紹介をします。料理の名前は”recipe.name”で、その材料は<li>これとこれとこれと….</li>です。
という感じになります。

このコードはコンパクトですが、例えば「麻婆豆腐に使う香辛料のリストだけ出して?」などの複雑なリクエストに対応しにくい欠点もあります。
また、コンポーネントの単一責任の原則に照らすと、このコードの役割が「料理リストを作る」「材料リストを作る」の2つの仕事をしているとも読み取れるので、一つのコードに対して役割を乗せすぎであると言えるかも知れません。(このあたりは開発チームの方針にもよるみたいです)

ということで、今回は「材料だけを取り出す」コンポーネントを切り出しました。

const IngredientsList = ({ ingredients }) => (
  <ul>
    {ingredients.map((ingredient, index) => (
      <li key={index}>{ingredient}</li>
    ))}
  </ul>
);

このコードを順に解説すると以下です。

  • const IngredientsList = ({ ingredients }) => (
    ここでは、IngredientsListというコンポーネントを宣言し、ingredientsをpropsとして(引数として?)取ることを示します。

  • <ul> </ul>
    記載したい内容がリストなので、リストの始まりと終わりを示すHTMLタグをJSXで記述しています。(HTMLっぽいコードをJavaScriptで書けるのがJSXです)

  • {ingredients.map((ingredient, index) => (
    ここでは、引数として与えられたingredientsが配列であると想定し、その各要素のingredient(単数系)を取り出しながらindex(連番)を付与していきます。
    ここでindexを付与すると、例えば [1. トマト, 2.きゅうり…] のように順番に番号が付与されるため、若い順に並び替えるなどの操作が困難になります。しかし現実的にはトマトときゅうりを並び替えたくなるシーンは無さそうなので、このままいきます。

  • <li key={index}>{ingredient}</li>
    作成したindex(連番)を付与し、それぞれの材料”ingredient”をリスト形式で表示します。

このコード(IngredientsListコンポーネント)は、ひとことで言ってしまえば「材料を全部放り込めば、綺麗にならべてくれるマシーン」とでも言いましょうか。
それでは次のセクションでは、「材料を全部放り込む」方法を見ていきます。

2-2. 材料の配列をリスト化する

先ほど、以下のように解説しました。

  • const IngredientsList = ({ ingredients }) => (
    ここでは、IngredientsListというコンポーネントを宣言し、ingredientsをpropsとして(引数として?)取ることを示します。

では、今から材料投入口 { ingredients } に材料を全部放り込みます。

いじるのは、最初のコード

export default function RecipeList() {
  return (
    <div>
      <h1>Recipes</h1>
      {recipes.map((recipe) => (
        <div key={recipe.id}>
          <h2>{recipe.name}</h2>
        </div>
      ))}
    </div>
  );
}

このコードに処理を追加します。

const RecipeList = () => (
  <div>
    {recipes.map((recipe) => (
      <div key={recipe.id}>
        <h2>{recipe.name}</h2>
        <IngredientsList ingredients={recipe.ingredients} />
      </div>
    ))}
  </div>
);

追加したのは、<IngredientsList ingredients={recipe.ingredients} /> の部分です。

これも切り分けて解説します。

  • <IngredientsList
    ここで、先ほど定義したIngredientsList コンポーネントを呼び出しています。

  • ingredients={recipe.ingredients} />
    ここで、propsとしてingredientsを定義し、その料理の材料(recipe.ingredients)を代入しています。
    言い換えれば、

    • ingredients(複数形)・・・材料を雑多に放り込める箱

    • {recipe.ingredients} ・・・その料理のレシピに書かれた材料ぜんぶ
      となります。箱に材料をどかっと入れたイメージですね。

箱詰めされた材料たちは、先ほど定義した「材料を全部放り込めば、綺麗にならべてくれるマシーン」に送られます。

3. 入れた材料が並ぶようにする

では、ここまでの順番を簡単に振り返ります。

const RecipeList = () => (
  <div>
    {recipes.map((recipe) => (
      <div key={recipe.id}>
        <h2>{recipe.name}</h2>
        <IngredientsList ingredients={recipe.ingredients} />
      </div>
    ))}
  </div>
);

最後に書いたこのコードは、簡単に言うと以下のことをしています。

  • 全部の料理をリスト化します

  • リスト化したそれぞれの料理について、IngredientsListコンポーネントに「材料ぜんぶ」の配列を渡して綺麗に並べてもらうよう依頼しています

で、材料をごっそり受け取ったIngredientsListコンポーネントの挙動は以下。

const IngredientsList = ({ ingredients }) => (
  <ul>
    {ingredients.map((ingredient, index) => (
      <li key={index}>{ingredient}</li>
    ))}
  </ul>
);
  • 届いた配列から材料を1個ずつ取り出す。これを全部に対して行う(map)

  • 1個ずつの材料に番号を振っておく(index)

  • それらの要素をリストに並べる(<ul>ならびに<li>タグ)

  • 出来上がったものを、依頼主に返す
    ※この部分は、1行目の => という表記によって実現しています。「IngredientsListコンポーネントは次の結果を返しますよ。」という宣言になっています。

まとめ

以上をまとめると、以下のようなことをやりました。

  • 【機能1】たくさんの料理からそれぞれの料理を1個ずつ取り出す

  • 【機能2】それぞれの料理から、それぞれの材料を取り出す機能部品を作る

  • 【機能3】機能1に機能2を入れ込む(ネスト)して、それぞれの料理のそれぞれの材料を取り出すようにする

料理取り出し機能の中に材料取り出し機能を内蔵する


配列の中の配列を処理するときは処理するコード自体をネストしても良いのですが、可読性が落ちたりコードが柔軟でなくなるおそれもあるので、配列内の配列を処理する専用のコンポーネントを作る考え方の方が、DRYかつ単一責任原則に則っていて良さげです。

今回の題材である教材はReactの公式チュートリアルです。

https://ja.react.dev/learn/rendering-lists

なお公式の模範解答の1つによると、配列の要素を取り出すコードをネストさせてもOKということのようです。
Reactの公式のガイドは、「結局どのようにレンダーされるかが大事で、途中経過は何でもいいよ。ただしこうやると遅くなるから避けたほうが良いかも。」といった、ゆるい感じで教えてくれるので好きです。

以上です。最後までお読みいただきありがとうございました。

この記事が気に入ったらサポートをしてみませんか?