🔁
循環参照データ作成でMaximum call stack size exceeded
概要
react native + expo + typescriptで下記のエラー。
[Unhandled promise rejection: RangeError: Maximum call stack size exceeded.]
このエラーは無限ループをするとよく発生するのですが、今回は目を皿のようにして何度も確認しましたが無限ループはありません。
結論からいうと(恐らく)循環参照が原因でした。
いわゆるナイーブツリーのデータをAPIから取得し、それぞれの子要素に親の参照を持たせようとしたら発生しました。
データ
わかりやすくデータは端折り、数も減らしました。実際はもっと大きなデータです。
{
"preferences": [
{
"id": 1,
"parentId": null,
"name": "ルートアイテム1",
"isLeaf": false,
"children": [
{
"id": 2,
"parentId": 1,
"name": "子要素1",
"isLeaf": false,
"children": [
{
"id": 3,
"parentId": 2,
"name": "孫要素1",
"isLeaf": true
}
]
}
]
}
]
}
問題のコード
// 各要素の型
export interface MatchingFactor {
id: number;
name: string;
isLeaf: boolean;
parentId?: number;
children?: MatchingFactor[];
parent?: MatchingFactor;
}
/**
* 実際にはreduxのreducerの中で読み込まれた時のみ一回だけ行なっています。
* action.payload.preferencesの中身は先ほどのデータ。
*/
mfFns.complementParents(action.payload.preferences);
// complementParentsの実装。
export const complementParents = (parents: MatchingFactor[]) => {
parents.forEach(parent => {
if (parent.children) {
// 子供たちに自分をセット。
parent.children.forEach(child => {
child.parent = parent;
});
complementParents(parent.children);
}
});
};
何となく動きそうなんですけど、parentがchildrenを持っていてその中に自分がいるので循環参照が起きてます。これを解消したら治ったので恐らくそれが原因です。
動いたコード
// 各要素の型
export interface MatchingFactor {
id: number;
name: string;
isLeaf: boolean;
parentId?: number;
children?: MatchingFactor[];
getParent: MatchingFactor | undefined;
}
export const complementParents = (parents: MatchingFactor[]) => {
parents.forEach(parent => {
// ルートの親には undefined を返す関数をセット。
if (isNotDefined(parent.getParent)) {
parent.getParent = () => undefined;
}
if (parent.children) {
// 子供たちに自分をかえす関数をセット。
parent.children.forEach(child => {
child.getParent = () => parent;
});
complementParents(parent.children);
}
});
}
参照をそのまま持たすのではなく参照を返す関数を持たせたら動きました。
感想
Maximum call stack size exceeded
自体は関数を再起的に呼び出して、条件ミスって無限ループした時にでるので、なにかミスってるかと何度も見直しました。
またこちらの記事にもあるりますが、容量オーバー的でも発生するよう。記事はapplyのRangeErrorなので少し趣が違うが一応データをサーバー側でLIMITして最小限にして試しても発生。
ただ、それをしたことによりconsole.log()
でデータを見渡せるようになり、children: [Circular]
となっているのを見つけて、もしやと思って関数にしたら動いた次第。
Discussion
データをまずは
flatten
してそのあとにpopulate
するのがいいんじゃないかと思いました。children
はoptional
でOmittable
にするのではなく、必須にして空配列を初期値で入れておくのがいいんじゃないかとも思いました。データ構造
ワークアラウンド
テストコード
demo code.
ありがとうございます!
今別案件でバタバタしてるので細かいところ見てませんが時間できたらやってみます。