react-component / react-component/util

isEqual: false 'circular references' warning for a reference repeated across sibling keys

Open Beginner friendly
#816 0 comments 0 reactions 0 assignees View on GitHub

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.