🔁
フラットノードに変換してReactで扱う設計入門【ネストしたデータはつらい?】
はじめに
Reactでツリー構造のUI(フォルダ構造、コメント欄、カテゴリ階層など)を扱っていると、次のような違和感を覚えたことはないでしょうか。
- ネストが深くなるとデータ更新がつらい
- 特定のノードを探すだけなのに処理が複雑
- state更新が直感的に書けない
- 再帰だらけで「これで合っているのか」不安になる
多くの場合、その原因は 「ネストしたデータ構造をそのまま扱おうとしていること」 にあります。
ネストしたデータ構造とは何か
まずは、よくあるネスト構造の例を見てみましょう。
ts
type Category = {
id: string
name: string
children?: Category[]
}
const categories: Category[] = [
{
id: "1",
name: "Frontend",
children: [
{
id: "2",
name: "React",
children: [
{ id: "3", name: "Next.js" }
]
}
]
}
]
一見すると直感的で、UIにもそのまま使えそうです。
実際、描画だけであれば問題ありません。
ネスト構造が「つらくなる」瞬間
1.特定のノードを更新したいとき
例えば、
- id === "3" のノード名を変更したい
この場合、どうしますか? - ループ
- 再帰
- children を辿る処理
が必要になります。
ts
function updateCategory(
nodes: Category[],
targetId: string,
newName: string
): Category[] {
return nodes.map(node => {
if (node.id === targetId) {
return { ...node, name: newName }
}
if (node.children) {
return {
...node,
children: updateCategory(node.children, targetId, newName)
}
}
return node
})
}
例として、上記のように毎回、再帰処理を書くのは現実的でしょうか?
2. 状態管理との相性が悪い
Reactでは「stateはなるべくシンプルに保つ」ことが重要です。
しかしネスト構造の場合、
- 深い階層まで immutability を保つ
- 一部だけ更新する
という操作が非常に煩雑になります。
これは Redux / Zustand / useReducer などでも同様です。
フラットノードという考え方
そこで登場するのが フラットノード構造 です。
フラットノードとは
- 各ノードを 1階層の配列 で管理
- 親子関係は
parentIdで表現
ts
type FlatCategory = {
id: string
name: string
parentId: string | null
}
データは次のようになります。
ts
const flatCategories: FlatCategory[] = [
{ id: "1", name: "Frontend", parentId: null },
{ id: "2", name: "React", parentId: "1" },
{ id: "3", name: "Next.js", parentId: "2" }
]
フラット構造のメリット
1. 特定ノードの取得が簡単
ts
const target = flatCategories.find(c => c.id === "3")
2. 更新処理が単純
ts
const updated = flatCategories.map(c =>
c.id === "3" ? { ...c, name: "Next.js App Router" } : c
)
3. 状態管理と相性が良い
- 正規化されたデータ
- Reduxの思想とも一致
- 再帰を「データ管理」から排除できる
では、ツリーUIはどう描画するのか?
ここで疑問が生まれます。
フラットにしたら、ツリー表示できないのでは?
答えは 「描画時だけ再帰を使う」 です。
フラットノードをツリーに変換する
まず、フラット構造をツリー構造に変換します。
ts
type TreeCategory = FlatCategory & {
children: TreeCategory[]
}
ts
function buildTree(
nodes: FlatCategory[],
parentId: string | null = null
): TreeCategory[] {
return nodes
.filter(node => node.parentId === parentId)
.map(node => ({
...node,
children: buildTree(nodes, node.id)
}))
}
これが 再帰関数の典型例です。
- 終了条件:該当する子がいなくなる
- 自己呼び出し:
buildTree(nodes, node.id)
ReactでツリーUIを描画する
再帰コンポーネント
ts
type Props = {
nodes: TreeCategory[]
}
const CategoryTree: React.FC<Props> = ({ nodes }) => {
return (
<ul>
{nodes.map(node => (
<li key={node.id}>
{node.name}
{node.children.length > 0 && (
<CategoryTree nodes={node.children} />
)}
</li>
))}
</ul>
)
}
使用例
ts
const tree = buildTree(flatCategories)
<CategoryTree nodes={tree} />
なぜ「フラット管理 × 再帰描画」が強いのか
責務が明確になる
| 役割 | 内容 |
|---|---|
| データ管理 | フラット構造 |
| 関係性 | parentId |
| UI描画 | 再帰 |
| 再帰が「局所化」される |
- 再帰は UI変換のためだけ
- state更新ロジックには登場しない
- バグの温床になりにくい
実務でよく使われる例
- コメント欄(X / Reddit / Qiita)
- フォルダツリー
- 権限・ロール管理
- メニュー構造
- 組織図
多くのバックエンドAPIも フラット構造で返却 します。
理由は同じです。
まとめ
- ネストしたデータは、更新・探索・状態管理がつらくなる
- フラットノード構造は、Reactと非常に相性が良い
- 再帰は データ管理ではなく描画や変換で使う
- 「フラットで持ち、ツリーで描く」が基本設計
Discussion