isEqual: false 'circular references' warning for a reference repeated across sibling keys
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 88/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
- Domain
- tooling
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.
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.
- Dominant language
- TypeScript
- Stars
- 670
- Forks
- 205
- Avg merge
- 15d 17h
- Merged PRs (30d)
- 6
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.
More from react-component/util
-
injectCSS method not attaching csp nonce passed to it. it is used in antd components internally Open
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
react-component/util#671 · 1 comment ·
-
Difficulty 4/5 3-5 days Newbie friendliness 35/100
react-component/util#738 · 1 comment · 1 reaction ·
-
Difficulty 4/5 3-5 days Newbie friendliness 25/100
react-component/util#710 ·
-
Difficulty 3/5 1-2 days Newbie friendliness 25/100
react-component/util#647 · 5 comments ·
-
Difficulty 3/5 1-2 days Newbie friendliness 45/100
react-component/util#644 · 1 comment ·
All issues in react-component/util
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Eynzof/Hermes-CN-Desktop#610 ·
-
bug clawsweeper:linked-pr-open clawsweeper:needs-live-repro clawsweeper:no-new-fix-pr impact:message-loss issue-rating: 🐚 platinum hermit P2 regression
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
calcite-components needs triage refactor
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Esri/calcite-design-system#15203 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 78/100
fullcalendar/fullcalendar#8106 ·