"too much recursion" when objects recursively reference each other
Version
3.2.29
Reproduction link
Steps to reproduce
When launching the repro in the SFC Playground, you will immediately notice the error "too much recursion". There are no additional steps required.
What is expected?
I expected to see no errors.
What is actually happening?
In the SFC Playground, you will notice the error "too much recursion". When running it locally, you might see something like this:
Uncaught (in promise) RangeError: Maximum call stack size exceeded
at Function.[Symbol.hasInstance] (<anonymous>)
at isDate (shared.esm-bundler.js?99c4:485:1)
at looseEqual (shared.esm-bundler.js?99c4:386:1)
at looseEqual (shared.esm-bundler.js?99c4:413:1)
at looseEqual (shared.esm-bundler.js?99c4:413:1)
at looseEqual (shared.esm-bundler.js?99c4:413:1)
at looseEqual (shared.esm-bundler.js?99c4:413:1)
at looseEqual (shared.esm-bundler.js?99c4:413:1)
at looseEqual (shared.esm-bundler.js?99c4:413:1)
at looseEqual (shared.esm-bundler.js?99c4:413:1)
To me, it looks like Vue is trying to deep-compare these objects and does not track already visited ones.
This is a data structure you might see e.g. when dealing with tree-like structures that are aware of their parents. One solution would be to filter out the "parent" information. But that could be non-trivial when using more complex data structures e.g. generated by a library.
Thank you for the reply! 👍
If the data structure is something I can control, then this certainly works. But if the data structure is generated by some kind of (external) library, then you have to make the entire data structure non-reactive. Which of course does not really work if you are using this data structure in other parts of your application where you depend on reactivity.
I must say that I was a bit surprised in general that no simply identity check (===
) is performed here. If you look at my example: If a
and b
have the same shape, then it is not possible to pre-select b
. This probably happens because Vue does not perform identity checks, but rather compares object shapes. Is it possible to change this behavior?
We do identity checks, but they fail as a raw original is being compared to its reactive proxy. So the check falls back to a shape comparison, which then runs into an endless loop because of the circular self-references.
Definitely solvable but needs a bit of evaluation to not break other edge cases.
Hello, any news on this please? Any schedule fix or work on it? I'm experiencing the issue with an ORM like library where instances reference each other.
Hello, I have basically the same problem as @paul-thebaud, I've been trying several workarounds by now but haven't gotten a real solution yet.
Does any of you have at least a suggestion?
My particular problem is that I'm referencing back the "parent" entity I'm dealing with in order to compute things based on its state changes.
I actually make it work in basic cases, but when the parent-child relation goes beyond 3 steps deep, a "too much recursion" error appears and blocks everything.
Example:
Lead -> Deal -> SaleOrder -> Item
parent -> child
-> parent -> child
-> parent -> child
The first 2 recursive links work well, but the third one breaks everything.
During my experiments, I've noticed a few things.
if I inject the parent like this
Object.defineProperty(child, path, { value: parent })
It actually does not break since it's not enumerable, but now the child doesn't notice the parent changes so nothing is recomputed. I'm looking for a simple way to maybe inject only a property that triggers this re-render process when the parent changes.
Any ideas here?
Maybe inject a forceUpdate (render) function in child obj to be triggered from parent?
By the way @posva the markRaw
function doesn't change anything for me I'm still getting the "Recursion Hell"
If it helps for anyone out there, I figure a solution out of this using the following approach.
- Stop using watchers. This makes me manually ensure the propagation of the changes, to do so I centralized the values mutation in the root, and all the required changes were pushed back to its parent using a
{path: string, value: any}
strategy to finally update back the root object in a single place and notify all children back with the regular Vue Observable proxy. - I ensured deleting the parent from the children's properties and injected it back using a none enumerable getter:
assignParent (child, path, parent) {
if (!path || !child || child.constructor.name === 'Object') return
if (!(path in child) || !child[path]) {
delete child[path]
Object.defineProperty(child, path, { get () { return parent }, enumerable: false })
}
}
In my case, all the data structures are instances of an entity Class so I double-check this to mitigate other problems.
- Once the new value is applied in the corresponding path, I notify the children's tree of it to "forceUpdate" only if required..
Does any of you have at least a suggestion?
instead of Object.defineProperty
, I solved this problem with markRaw
as hinted above - not sure why it doesn't work for you. In my case, I changed my pinia getter from
{
...,
children: [...],
parent: parent
}
to
{
...,
static: markRaw({
children: [...],
},
parent: parent
}
and then access children as x.static.children
whenever necessary. It's not reactive anymore then, true, but that's probably not a dealbreaker?
the overall point is: this only happens when these two conditions are met:
- a raw object is being compared to its reactive proxy counterpart with the internal
looseEqual()
helper. on the user-facing side of things, this can only happen when using checkboxes with explicitly providedtrue-value
/false-value
or v-model on select elements. - the object contains circular references, i.e. a self-reference
Thus, as a workaround, you can either make all your data raw or reactive.
playground (note lines 4+5)
As to why this hasn't been solved yet: it's a bit tricky. The most straightforward solution would to make looseEqual
also check the raw values if any of the comparators are reactive proxies. But since we use the looseEqual()
function in quite a few other places internally, we have to verify that "rawObj
=== itsReactiveProxy
// => true`" is not creating a problem in those other places (or the one shown in OP).
And we simply haven't gotten around to do that, since this is also kind of an edge case and can be worked around as shown above