何をしたいユーティリティか:「配列のネスト化」
ここでの「ネスト化」は、平らな配列を「階層構造(入れ子)」に組み立て直す処理のことです。
フラット化の逆方向だと思ってください。
例えば、次のようなことをしたくなります。
カテゴリ一覧を「親カテゴリ → 子カテゴリ」のツリー構造にしたい。
部署一覧を「会社 → 部 → 課」の階層にしたい。
コメント一覧を「親コメント → 返信」のツリーにしたい。
業務では「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;
}
JavaScriptrowsOf(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 文の塊」から、「意図がはっきりしたツリー構築ロジック」に一段ステップアップしていきます。
