ネスト構造のJSONをReactで扱うときにハマったポイントと解決策
はじめに
ReactでAPIから取得したJSONを元にUIを構築していたとき、「データが表示されない」「ドラッグ&ドロップで順番が変わらない」などの不具合に悩まされました。
原因は、APIから返ってくるデータが「ネスト構造」になっている点を踏まえずに実装したこと。
今回は、私が実際にハマったポイントと、それをどうやって解決したかを備忘録として記します。
ネスト構造のJSONを扱う可能性のある方、そしてD&Dやリストのソートなどを導入している方には参考になるかもしれません。
背景
-
React + Rails API で作ったメモアプリ
-
フォルダ構造は「親 → 子」のツリー形式
-
API側では
as_tree_json
によってネスト構造のJSONが返ってくる
ハマったポイント
useState
で管理しているfolders
は、初回レンダリング時にAPIレスポンスのデータを受け取ってセットされます。
しかし、このfolders
データは以下のようなネスト構造になっているため、コンポーネント内で.map()
や.filter()
といった処理をする際にうまく動かないケースが生じます。
[
{
id: 1,
name: "親",
parent_id: null,
position: 0,
children: [
{
id: 2,
name: "子A",
parent_id: 1,
position: 0
},
{
id: 3,
name: "子B",
parent_id: 1,
position: 1
}
]
}
]
結果、諸々の処理の噛み合わせが悪くなり、フォルダ一覧を表示させたいダッシュボードページにおいて、初回レンダリング時、保存されているはずの同じparent_id
を持つフォルダたちが表示されない不具合が起きました。
解決策
useEffect
内でネスト構造を以下のようにflatten(平坦化)してsetFolders
する必要があります。
[
{ id: 1, name: "親", parent_id: null, position: 0 },
{ id: 2, name: "子A", parent_id: 1, position: 0 },
{ id: 3, name: "子B", parent_id: 1, position: 1 }
]
flattenFolders関数の概観
function flattenFolders(tree: Folder[]): Folder[] {
const result: Folder[] = [];
function walk(node: Folder) {
const { children, ...rest } = node;
result.push(rest);
if (children && Array.isArray(children)) {
children.forEach(walk);
}
}
tree.forEach(walk);
return result;
}
flattenFolders関数の解読
function flattenFolders(tree: Folder[]): Folder[] {
tree
にはネスト構造のフォルダ配列が渡される想定。
戻り値は平坦化されたフォルダ配列。
const result: Folder[] = [];
最終的に返すための、「平坦化したフォルダたち」を入れる空箱。
ここにフラットな形でフォルダを追加していく。
function walk(node: Folder) {
walk
は再帰関数(自分自身を呼ぶ関数)。
walk
やnode
という名前は、ツリー構造(ネスト構造)を再帰的にたどる処理でよく使われる「慣習的な名前」。
フォルダをひとつずつ辿っていく役割。
1つのフォルダを処理して、必要ならその子供も見にいく。
const { children, ...rest } = node;
これは「分割代入」という書き方。
node
(フォルダ)のchildren
を取り出して、残りの情報(id, name
, parent_id
など)を...rest
でまとめている。
rest
は { id, name, parent_id, position, ... } みたいな形になる。
result.push(rest);
children
を除いた情報を result
に追加(この時点で「親フォルダだけ」が追加される)。
if (children && Array.isArray(children)) {
children.forEach(walk);
}
Array.isArray()
は、引数が配列かどうかを判定する関数。
もしchildren
があり、中に配列がある(子フォルダが入っている)なら、それぞれの子フォルダにもwalk()
を使って再帰的に処理する。
→ これにより親→子→孫…すべての階層をフラットにたどれる。
tree.forEach(walk);
最初にもらったtree
のトップレベルのフォルダをすべて walk()
で処理開始。
return result;
最終的に、フラットになったフォルダ配列が返ってくる。
flattenFolders関数の使い方
useEffect(() => {
fetch('http://localhost:3000/api/folders')
.then((res) => res.json())
.then((data) => {
const flat = flattenFolders(data);
setFolders(flat);
})
.catch((err) => {
console.error('フォルダ取得に失敗しました', err);
});
}, []);
useEffect
内にて、flattenFolders
関数でdata
をフラット構造にし、setFolders
で保存する。
おまけ:リアルなネスト構造図
flattenFolders関数の挙動を頭でイメージしやすくするために、実際のアプリでよくある「ごちゃついたリアルなネスト構造」を掲載しておきます(完全自分用)。
[
{
"id": 1,
"name": "トップ",
"parent_id": null,
"position": 0,
"children": [
{
"id": 3,
"name": "企画資料",
"parent_id": 1,
"position": 0,
"children": [
{
"id": 12,
"name": "2024年企画",
"parent_id": 3,
"position": 0
},
{
"id": 24,
"name": "2025年企画",
"parent_id": 3,
"position": 1
}
]
},
{
"id": 9,
"name": "デザイン",
"parent_id": 1,
"position": 1
},
{
"id": 45,
"name": "開発資料",
"parent_id": 1,
"position": 2,
"children": [
{
"id": 46,
"name": "フロントエンド",
"parent_id": 45,
"position": 0
},
{
"id": 47,
"name": "バックエンド",
"parent_id": 45,
"position": 1,
"children": [
{
"id": 48,
"name": "Railsメモ",
"parent_id": 47,
"position": 0
}
]
}
]
}
]
}
]
最後に
.map()
や.filter()
のメソッドが、一次元配列にしか有効でないなんて知らなかった。
Discussion