react-component / react-component/util
isEqual: false 'circular references' warning for a reference repeated across sibling keys
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 670
- Forks
- 205
- Avg merge
- 11d 17h
- Merged PRs (30d)
- 4
Description
isEqual reports "There may be circular references" and returns false for deeply-equal objects
Package: rc-util@5.44.4 and @rc-component/util@1.13.0 (both current; identical logic in src/isEqual.ts)
What happens
isEqual returns false for two objects that are deeply equal, and logs Warning: There may be circular references, when one of them holds the same reference in two different keys. Neither object contains a cycle.
import isEqual from 'rc-util/lib/isEqual';
const shared = [];
const a = { errors: shared, warnings: shared }; // one array, two keys
const b = { errors: [], warnings: [] }; // same value, two separate arrays
isEqual(a, b);
// → false
// → console.error: "Warning: There may be circular references"
JSON.stringify(a) === JSON.stringify(b); // → true
The same happens for any repeated reference, not just arrays:
const point = { x: 1 };
isEqual({ a: point, b: point }, { a: { x: 1 }, b: { x: 1 } }); // → false, warns
Why
refSet is meant to detect cycles, but it records every value the walk has ever visited and never removes anything when the walk leaves a branch:
const refSet = new Set<any>();
function deepEqual(a: any, b: any, level = 1): boolean {
const circular = refSet.has(a);
warning(!circular, 'Warning: There may be circular references');
if (circular) {
return false;
}
...
refSet.add(a); // added on the way in, never removed on the way out
A cycle means a value reachable from itself — that is, a value that is its own ancestor along the current path. The set therefore needs to hold the current path, not the whole history. As written, a value legitimately reached twice in two sibling branches is indistinguishable from a cycle: the second visit finds it in the set, warns, and returns false.
Why it matters in practice
rc-field-form shares one empty-array constant between two fields of a field's meta (Field.tsx):
const EMPTY_ERRORS: any[] = [];
...
public errors: string[] = EMPTY_ERRORS;
public warnings: string[] = EMPTY_ERRORS;
Field.triggerMetaEvent then compares the previous meta with the next via isEqual. Once a field has validated, its errors is a fresh [] while another field's meta still carries the shared constant in both keys — so the comparison hits exactly the case above.
The visible result in any antd app is a Warning: There may be circular references in the dev console after a programmatic form.setFieldValue(...), pointing at application code that has no circular data of any kind. The functional effect is milder but real: isEqual returns false for metas that are equal, so onMetaChange fires when nothing changed.
Suggested fix
Track ancestors rather than history — remove the entry once the walk leaves the branch:
refSet.add(a);
const newLevel = level + 1;
- if (Array.isArray(a)) {
- ...
- }
- // other
- return false;
+ try {
+ if (Array.isArray(a)) {
+ ...
+ }
+ // other
+ return false;
+ } finally {
+ // `a` is an ancestor only while the walk is inside it; a value reached again in a sibling
+ // branch is a repeat, not a cycle.
+ refSet.delete(a);
+ }
}
A full patch is attached, with cases added to the existing src/test/isEqual.test.ts. Every test already in that file passes unchanged, including should not equal 6, which is the cyclic one. With the change:
| case | before | after |
|---|---|---|
| one array held in two keys | false, warns |
true, silent |
| one object reused across sibling keys | false, warns |
true, silent |
a genuine cycle (a.self = a) |
false, warns |
false, warns |
| plainly equal / unequal values | unchanged | unchanged |
Happy to open the PR if the approach looks right.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the ancestor-tracking logic in src/isEqual.ts and the existing cases in src/test/isEqual.test.ts. Run the isEqual tests, then verify repeated references in sibling keys compare equal without a warning while a genuine cycle still returns false and warns; existing equal and unequal cases should remain unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 88/100