JavaScript Tips | 配列ユーティリティ:ネスト化

JavaScript JavaScript
スポンサーリンク

何をしたいユーティリティか:「配列のネスト化」

ここでの「ネスト化」は、平らな配列を「階層構造(入れ子)」に組み立て直す処理のことです。
フラット化の逆方向だと思ってください。

例えば、次のようなことをしたくなります。
カテゴリ一覧を「親カテゴリ → 子カテゴリ」のツリー構造にしたい。
部署一覧を「会社 → 部 → 課」の階層にしたい。
コメント一覧を「親コメント → 返信」のツリーにしたい。

業務では「DB からはフラットな行として取れるけど、画面ではツリーで表示したい」という場面が本当に多いです。
そこで、「ネスト化ユーティリティ」を持っておくと、毎回同じロジックを書かずに済みます。


パターン1:固定サイズで「段組み」的にネスト化する

イメージ:「1 行 3 件」のような二次元配列にする

まずはシンプルなパターンからいきます。
配列を「1 行 3 件」のように並べたいとき、フロント側では「二次元配列」にしておくと扱いやすくなります。

例えば、次の配列があります。

const items = [1, 2, 3, 4, 5, 6, 7];
JavaScript

これを「1 行 3 件」でネスト化すると、こうなります。

[
  [1, 2, 3],
  [4, 5, 6],
  [7],
]
JavaScript

これは、以前やった「分割(chunk)」の結果そのものです。
つまり、「分割ユーティリティ」はそのまま「ネスト化ユーティリティ」としても使える、ということです。

実装例:rowsOf(行ごとにネスト化)

function rowsOf(array, columns) {
  if (!Array.isArray(array)) {
    return [];
  }

  const cols = columns > 0 ? columns : 1;
  const result = [];

  for (let i = 0; i < array.length; i += cols) {
    result.push(array.slice(i, i + cols));
  }

  return result;
}
JavaScript

rowsOf(items, 3) とすれば、「1 行 3 件」の二次元配列になります。
この形にしておくと、テンプレート側で「行をループ → 行の中の要素をループ」という書き方ができて、UI コードがとても素直になります。


パターン2:親子関係からツリー構造にネスト化する

一番よく出る「id / parentId からツリーを作る」問題

業務で一番よく出るネスト化は、「id と parentId を持つフラットな配列からツリーを作る」パターンです。

例えば、次のようなカテゴリ一覧があります。

const categories = [
  { id: 1, name: "家電", parentId: null },
  { id: 2, name: "テレビ", parentId: 1 },
  { id: 3, name: "冷蔵庫", parentId: 1 },
  { id: 4, name: "本", parentId: null },
  { id: 5, name: "小説", parentId: 4 },
];
JavaScript

これを、次のようなツリー構造にしたいとします。

[
  {
    id: 1,
    name: "家電",
    parentId: null,
    children: [
      { id: 2, name: "テレビ", parentId: 1, children: [] },
      { id: 3, name: "冷蔵庫", parentId: 1, children: [] },
    ],
  },
  {
    id: 4,
    name: "本",
    parentId: null,
    children: [
      { id: 5, name: "小説", parentId: 4, children: [] },
    ],
  },
]
JavaScript

「親の下に children 配列をぶら下げる」という形です。
これができると、ツリービューや階層メニューを簡単に描画できます。

実装例:buildTree(id / parentId からツリーを構築)

function buildTree(items, { idKey = "id", parentKey = "parentId", childrenKey = "children" } = {}) {
  if (!Array.isArray(items)) {
    return [];
  }

  const byId = new Map();
  const roots = [];

  for (const raw of items) {
    if (!raw || typeof raw !== "object") continue;
    const item = { ...raw, [childrenKey]: [] };
    byId.set(item[idKey], item);
  }

  for (const item of byId.values()) {
    const parentId = item[parentKey];

    if (parentId == null) {
      roots.push(item);
      continue;
    }

    const parent = byId.get(parentId);
    if (parent) {
      parent[childrenKey].push(item);
    } else {
      roots.push(item);
    }
  }

  return roots;
}
JavaScript

ここが重要ポイントです。

最初のループで、「id → 要素」の Map を作りつつ、全要素に空の children を付けています。
2 回目のループで、「親がいるなら親の children に push」「親がいない(parentId が null / undefined、または見つからない)ならルート扱い」というルールでツリーを組み立てています。

この 2 段階構成にすることで、「親より子が先に出てくる」ような順番でも正しくツリーを作れます。

実際の動き

const tree = buildTree(categories);

console.dir(tree, { depth: null });
JavaScript

これで、先ほどイメージしたツリー構造が得られます。
childrenKey"children" 以外にしたい場合(例えば "nodes")は、オプションで変えられるようにしてあります。


パターン3:任意の「キーの組み合わせ」で段階的にネスト化する

例:「国 → 都道府県 → 市区町村」のような多段階ネスト

もう少し複雑な例として、「複数のキーで段階的にネスト化する」パターンを考えます。

例えば、次のような住所データがあります。

const addresses = [
  { country: "Japan", prefecture: "Tokyo", city: "Shinjuku" },
  { country: "Japan", prefecture: "Tokyo", city: "Edogawa" },
  { country: "Japan", prefecture: "Osaka", city: "Kita" },
  { country: "USA", prefecture: "CA", city: "San Francisco" },
];
JavaScript

これを、「国 → 都道府県 → 市区町村」の三段階ネストにしたいとします。

[
  {
    country: "Japan",
    children: [
      {
        prefecture: "Tokyo",
        children: [
          { city: "Shinjuku" },
          { city: "Edogawa" },
        ],
      },
      {
        prefecture: "Osaka",
        children: [
          { city: "Kita" },
        ],
      },
    ],
  },
  {
    country: "USA",
    children: [
      {
        prefecture: "CA",
        children: [
          { city: "San Francisco" },
        ],
      },
    ],
  },
]
JavaScript

ここでは、「どのキーでどの順番にネストするか」を配列で渡せるようにすると便利です。

実装例:nestByKeys(キーの配列で段階的にネスト)

function nestByKeys(items, keys, childrenKey = "children") {
  if (!Array.isArray(items)) {
    return [];
  }
  if (!Array.isArray(keys) || keys.length === 0) {
    return items.slice();
  }

  function nestLevel(currentItems, level) {
    if (level >= keys.length) {
      return currentItems.slice();
    }

    const key = keys[level];
    const groups = new Map();

    for (const item of currentItems) {
      const groupKey = item[key];
      const k = groupKey == null ? "__undefined__" : String(groupKey);
      if (!groups.has(k)) {
        groups.set(k, []);
      }
      groups.get(k).push(item);
    }

    const result = [];

    for (const [groupKey, groupItems] of groups.entries()) {
      const sample = groupItems[0] || {};
      const node = { [key]: sample[key] };
      node[childrenKey] = nestLevel(groupItems, level + 1);
      result.push(node);
    }

    return result;
  }

  return nestLevel(items, 0);
}
JavaScript

ここが重要ポイントです。

keys は、「この順番でネストしていくキー名の配列」です。
再帰関数 nestLevel が、「今のレベルのキーでグループ化 → 次のレベルを children として再帰的に構築」という処理をしています。
groupKey == null の場合は "__undefined__" という特別なキーにまとめておき、「キーがないデータ」も一応どこかに入るようにしています。

このユーティリティは、「ツリーのノードに何を持たせるか」をかなりシンプルにしていますが、
実務では「元の item を丸ごと持たせる」「集計値を持たせる」など、用途に応じて拡張できます。


「ネスト化」で特に意識してほしい重要ポイント

1. 「構造を作る」ことと「見た目を作る」ことを分ける

ネスト化ユーティリティは、「データの構造」を作る役割に徹させるのがコツです。
「どのキーでグループ化するか」「親子関係をどうつなぐか」はユーティリティ側でやり、
「どう表示するか(インデント、アイコン、折りたたみなど)」は UI 側でやる、という分担にすると、コードがきれいに分かれます。

ツリー表示で困っているときは、「まずフラットな配列を、ツリー構造の配列に変換する」ことだけに集中してみてください。
表示はそのあとで考える、という順番がうまくいきやすいです。

2. 「親がいないデータ」をどう扱うかを決める

buildTree のようなユーティリティでは、「parentId が指す親が存在しない」データが普通に出てきます。
ここをどう扱うかを、ユーティリティ側で決めておくのが大事です。

ルートとして扱う(今回の実装)
エラーとしてログに出す
無視する

どれが正解かは要件次第ですが、「仕様としてどうするか」を一度決めてしまうと、呼び出し側が迷わなくなります。

3. 「id / parentId の循環」に注意する

現実のデータでは、「A の親が B、B の親が A」のような循環参照が紛れ込むことがあります。
単純な実装だと、再帰が無限ループになってしまう危険があります。

今回の buildTree は、「親を探すときに Map を 1 回見るだけ」で、再帰を使っていないので、循環していても無限ループにはなりません。
このように、「ツリー構築ロジックはできるだけ非再帰で書く」か、「再帰する場合は訪問済みチェックを入れる」ことを意識しておくと安全です。


少し手を動かして感覚をつかむ

コンソールで、次のようなコードを実際に打ってみてください。

const items = [1, 2, 3, 4, 5, 6, 7];
rowsOf(items, 3);

const categories = [
  { id: 1, name: "家電", parentId: null },
  { id: 2, name: "テレビ", parentId: 1 },
  { id: 3, name: "冷蔵庫", parentId: 1 },
  { id: 4, name: "本", parentId: null },
  { id: 5, name: "小説", parentId: 4 },
];

const tree = buildTree(categories);

const addresses = [
  { country: "Japan", prefecture: "Tokyo", city: "Shinjuku" },
  { country: "Japan", prefecture: "Tokyo", city: "Edogawa" },
  { country: "Japan", prefecture: "Osaka", city: "Kita" },
  { country: "USA", prefecture: "CA", city: "San Francisco" },
];

const nested = nestByKeys(addresses, ["country", "prefecture", "city"]);
JavaScript

「フラットな配列が、どういう形のネスト構造に変わるか」「children がどうぶら下がっているか」を、自分の目で確認してみてください。

そのうえで、自分のプロジェクトに

export function rowsOf(...) { ... }
export function buildTree(...) { ... }
export function nestByKeys(...) { ... }
JavaScript

のような関数を置き、「階層構造を作りたくなったら、必ずこの“ネスト化ユーティリティ”を通す」というルールを作ってみてください。
そうすると、あなたのコードは、「場当たり的な for 文と if 文の塊」から、「意図がはっきりしたツリー構築ロジック」に一段ステップアップしていきます。

タイトルとURLをコピーしました