Skip to content

Commit 2957503

Browse files
Stop infinite renders caused by circular $ref object properties
Fixes #3907, #4262 and #3826 - Adds RJSF_REF_CYCLE_KEY ('__rjsf_ref_cycle') to detect and terminate self-referential schemas that would otherwise render infinitely. - In resolveAllReferences, when a $ref is encountered that's already in the recurseList AND we're resolving in simple (non-resolveAnyOfOrOneOfRefs) mode, the schema is tagged with __rjsf_ref_cycle: true instead of silently returning the unresolved $ref. This only fires in a direct object-property context (!resolveAnyOfOrOneOfRefs), where rendering is unconditional and cycles are genuinely infinite. Array-items and anyOf/oneOf contexts remain untagged since they are data-driven and terminate naturally. - SchemaField checks _schema[RJSF_REF_CYCLE_KEY] after its useCallback hook (to satisfy React rules of hooks) and renders a CyclicSchemaField instead of recursing. CyclicSchemaField renders the CyclicSchemaExpandTemplate, which shows the user a message and an expand button to load the next cycle break. - hashForSchema now filters out keys starting with RJSF_REF_KEY before hashing so that internal cycle-tracking metadata does not affect the computed hash. - CyclicSchemaExpandTemplate is implemented for all theme packages: antd, chakra-ui, daisyui, fluentui-rc, mantine, mui, primereact, react-bootstrap, semantic-ui, and shadcn, each using their native UI library components. - Fixed buttonId in all CyclicSchemaExpandTemplate implementations to use fieldPathId[ID_KEY] (the $id string) instead of fieldPathId (the object). Button id format is now `${fieldPathId.$id}-button` (e.g. `root_child_child-button`), dropping the redundant `-${name}-` segment. - Added SchemaField test verifying that clicking the expand button renders the next level of the cycle, finding the button by its correct DOM id. - Updated FormSnap snapshot to reflect the corrected button id. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3b8c8cc commit 2957503

68 files changed

Lines changed: 3627 additions & 834 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,65 @@ should change the heading of the (upcoming) version to include a major version b
1616
1717
-->
1818

19+
# 6.7.0
20+
21+
## @rjsf/antd
22+
23+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
24+
25+
## @rjsf/chakra-ui
26+
27+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
28+
29+
## @rjsf/core
30+
31+
- Fixed [#3907](https://github.com/rjsf-team/react-jsonschema-form/issues/3907) and [#4262](https://github.com/rjsf-team/react-jsonschema-form/issues/4262) as follows:
32+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
33+
- Added `CyclicSchemaField` to the list of fields, that renders the `CyclicSchemaExpandTemplate` initially and, if expanded, will render the `SchemaField` with the `RJSF_REF_CYCLE_KEY` tag turned off
34+
- Updated `SchemaForm` to render the `CyclicSchemaField` when the schema contains the `RJSF_REF_CYCLE_KEY` set to `true`
35+
36+
## @rjsf/daisyui
37+
38+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
39+
40+
## @rjsf/fluentui-rc
41+
42+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
43+
44+
## @rjsf/mantine
45+
46+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
47+
48+
## @rjsf/mui
49+
50+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
51+
52+
## @rjsf/primereact
53+
54+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
55+
56+
## @rjsf/react-bootstrap
57+
58+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
59+
60+
## @rjsf/semantic-ui
61+
62+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
63+
64+
## @rjsf/shadcn
65+
66+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
67+
68+
## @rjsf/utils
69+
70+
- Updated `types.ts` to add `CyclicSchemaExpandProps` type and `CyclicSchemaExpandTemplate` in the `TemplatesType`
71+
- Updated `resolveAllReferences()` to add a new `markCycleOnDetection` prop which adds `RJSF_REF_CYCLE_KEY` marker (from `constants.ts`) to a schema that has been detected to have a cycle, partially fixing [#3907](https://github.com/rjsf-team/react-jsonschema-form/issues/3907)
72+
- Updated `hashForSchema()` to filter keys to remove `RJSF_REF_KEY` prefixed keys before hashing the schema
73+
- Updated `enums.ts` to add `ExpandButton` and `CycleDetected` keys
74+
75+
## Dev / docs / playground
76+
77+
- Updated `@rjsf/snapshots` to add a test case to `formTests` that verifies the new Cycle detection UI
1978
# 6.6.2
2079

2180
## @rjsf/core
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { CyclicSchemaExpandProps, FormContextType, RJSFSchema, StrictRJSFSchema } from '@rjsf/utils';
2+
import { ID_KEY, TranslatableString } from '@rjsf/utils';
3+
import { Alert, Button, Space, version } from 'antd';
4+
5+
const antdMajor = parseInt(version.split('.')[0], 10);
6+
7+
/** The `CyclicSchemaExpandTemplate` is the template to use to render the cyclic schema expand message and controls
8+
*
9+
* @param props - The `CyclicSchemaExpandProps` for this component
10+
*/
11+
export default function CyclicSchemaExpandTemplate<
12+
T = any,
13+
S extends StrictRJSFSchema = RJSFSchema,
14+
F extends FormContextType = any,
15+
>(props: CyclicSchemaExpandProps<T, S, F>) {
16+
const { name, fieldPathId, registry, onExpand } = props;
17+
const { translateString } = registry;
18+
const buttonId = `${fieldPathId[ID_KEY]}-button`;
19+
20+
const headerProp =
21+
antdMajor >= 6
22+
? { title: translateString(TranslatableString.CycleDetected, [name]) }
23+
: { message: translateString(TranslatableString.CycleDetected, [name]) };
24+
25+
return (
26+
<Alert
27+
type='warning'
28+
{...headerProp}
29+
action={
30+
<Space>
31+
<Button id={buttonId} size='small' type='default' onClick={() => onExpand(fieldPathId[ID_KEY])}>
32+
{translateString(TranslatableString.ExpandButton)}
33+
</Button>
34+
</Space>
35+
}
36+
/>
37+
);
38+
}

packages/antd/src/templates/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { FormContextType, RJSFSchema, StrictRJSFSchema, TemplatesType } fro
33
import ArrayFieldItemTemplate from './ArrayFieldItemTemplate';
44
import ArrayFieldTemplate from './ArrayFieldTemplate';
55
import BaseInputTemplate from './BaseInputTemplate';
6+
import CyclicSchemaExpandTemplate from './CyclicSchemaExpandTemplate';
67
import ErrorList from './ErrorList';
78
import DescriptionField from './FieldDescriptionTemplate';
89
import FieldErrorTemplate from './FieldErrorTemplate';
@@ -25,6 +26,7 @@ export function generateTemplates<
2526
ArrayFieldItemTemplate,
2627
ArrayFieldTemplate,
2728
BaseInputTemplate,
29+
CyclicSchemaExpandTemplate,
2830
ButtonTemplates: {
2931
AddButton,
3032
CopyButton,

packages/antd/test/__snapshots__/Form.test.tsx.snap

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4545,6 +4545,195 @@ exports[`nameGenerator > dotNotationNameGenerator > simple fields 1`] = `
45454545
</DocumentFragment>
45464546
`;
45474547

4548+
exports[`single fields > Cyclic schema 1`] = `
4549+
<DocumentFragment>
4550+
<form
4551+
class="rjsf"
4552+
>
4553+
<div
4554+
class="rjsf-field rjsf-field-object"
4555+
>
4556+
<div
4557+
class="ant-form-item css-var-root ant-form-css-var css-dev-only-do-not-override-ypkju9 ant-form-item-horizontal"
4558+
>
4559+
<div
4560+
class="ant-row ant-form-item-row css-dev-only-do-not-override-ypkju9 css-var-root"
4561+
>
4562+
<div
4563+
class="ant-col ant-col-24 ant-form-item-control css-dev-only-do-not-override-ypkju9 css-var-root"
4564+
>
4565+
<div
4566+
class="ant-form-item-control-input"
4567+
>
4568+
<div
4569+
class="ant-form-item-control-input-content"
4570+
>
4571+
<fieldset
4572+
id="root"
4573+
>
4574+
<div
4575+
class="ant-row css-dev-only-do-not-override-ypkju9 css-var-root"
4576+
style="margin-inline: -12px;"
4577+
>
4578+
<div
4579+
class="ant-col ant-col-24 ant-form-item-label css-dev-only-do-not-override-ypkju9 css-var-root"
4580+
style="padding-inline: 12px;"
4581+
>
4582+
<label
4583+
class=""
4584+
for="root__title"
4585+
title="A registration form"
4586+
>
4587+
A registration form
4588+
</label>
4589+
<div
4590+
class="ant-divider css-dev-only-do-not-override-ypkju9 css-var-root ant-divider-horizontal ant-divider-sm ant-divider-rail"
4591+
role="separator"
4592+
style="margin-block: 1px;"
4593+
/>
4594+
</div>
4595+
<div
4596+
class="ant-col ant-col-24 css-dev-only-do-not-override-ypkju9 css-var-root"
4597+
style="padding-inline: 12px;"
4598+
>
4599+
<div
4600+
class="rjsf-field rjsf-field-object"
4601+
>
4602+
<div
4603+
class="ant-form-item css-var-root ant-form-css-var css-dev-only-do-not-override-ypkju9 ant-form-item-horizontal"
4604+
>
4605+
<div
4606+
class="ant-row ant-form-item-row css-dev-only-do-not-override-ypkju9 css-var-root"
4607+
>
4608+
<div
4609+
class="ant-col ant-col-24 ant-form-item-control css-dev-only-do-not-override-ypkju9 css-var-root"
4610+
>
4611+
<div
4612+
class="ant-form-item-control-input"
4613+
>
4614+
<div
4615+
class="ant-form-item-control-input-content"
4616+
>
4617+
<fieldset
4618+
id="root_child"
4619+
>
4620+
<div
4621+
class="ant-row css-dev-only-do-not-override-ypkju9 css-var-root"
4622+
style="margin-inline: -12px;"
4623+
>
4624+
<div
4625+
class="ant-col ant-col-24 ant-form-item-label css-dev-only-do-not-override-ypkju9 css-var-root"
4626+
style="padding-inline: 12px;"
4627+
>
4628+
<label
4629+
class=""
4630+
for="root_child__title"
4631+
title="A registration form"
4632+
>
4633+
A registration form
4634+
</label>
4635+
<div
4636+
class="ant-divider css-dev-only-do-not-override-ypkju9 css-var-root ant-divider-horizontal ant-divider-sm ant-divider-rail"
4637+
role="separator"
4638+
style="margin-block: 1px;"
4639+
/>
4640+
</div>
4641+
<div
4642+
class="ant-col ant-col-24 css-dev-only-do-not-override-ypkju9 css-var-root"
4643+
style="padding-inline: 12px;"
4644+
>
4645+
<div
4646+
class="ant-alert ant-alert-warning ant-alert-outlined ant-alert-no-icon css-var-root css-dev-only-do-not-override-ypkju9"
4647+
data-show="true"
4648+
role="alert"
4649+
>
4650+
<div
4651+
class="ant-alert-section"
4652+
>
4653+
<div
4654+
class="ant-alert-title"
4655+
>
4656+
Circular reference ($ref cycle) detected for field "child". You may choose to expand to the next cycle break
4657+
</div>
4658+
</div>
4659+
<div
4660+
class="ant-alert-actions"
4661+
>
4662+
<div
4663+
class="ant-space css-dev-only-do-not-override-ypkju9 ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small css-var-root"
4664+
>
4665+
<div
4666+
class="ant-space-item"
4667+
>
4668+
<button
4669+
class="ant-btn css-dev-only-do-not-override-ypkju9 css-var-root ant-btn-default ant-btn-color-default ant-btn-variant-outlined ant-btn-sm"
4670+
id="root_child_child-button"
4671+
type="button"
4672+
>
4673+
<span>
4674+
Expand Cycle
4675+
</span>
4676+
</button>
4677+
</div>
4678+
</div>
4679+
</div>
4680+
</div>
4681+
</div>
4682+
</div>
4683+
</fieldset>
4684+
</div>
4685+
</div>
4686+
<div
4687+
class="ant-form-item-additional"
4688+
>
4689+
<div
4690+
class="ant-form-item-extra"
4691+
>
4692+
<span
4693+
id="root_child__description"
4694+
>
4695+
A simple form example.
4696+
</span>
4697+
</div>
4698+
</div>
4699+
</div>
4700+
</div>
4701+
</div>
4702+
</div>
4703+
</div>
4704+
</div>
4705+
</fieldset>
4706+
</div>
4707+
</div>
4708+
<div
4709+
class="ant-form-item-additional"
4710+
>
4711+
<div
4712+
class="ant-form-item-extra"
4713+
>
4714+
<span
4715+
id="root__description"
4716+
>
4717+
A simple form example.
4718+
</span>
4719+
</div>
4720+
</div>
4721+
</div>
4722+
</div>
4723+
</div>
4724+
</div>
4725+
<button
4726+
class="ant-btn css-dev-only-do-not-override-ypkju9 css-var-root ant-btn-submit"
4727+
type="submit"
4728+
>
4729+
<span>
4730+
Submit
4731+
</span>
4732+
</button>
4733+
</form>
4734+
</DocumentFragment>
4735+
`;
4736+
45484737
exports[`single fields > checkbox field 1`] = `
45494738
<DocumentFragment>
45504739
<form
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Box, Button } from '@chakra-ui/react';
2+
import type { CyclicSchemaExpandProps, FormContextType, RJSFSchema, StrictRJSFSchema } from '@rjsf/utils';
3+
import { ID_KEY, TranslatableString } from '@rjsf/utils';
4+
5+
import { Alert } from '../components/ui/alert';
6+
7+
/** The `CyclicSchemaExpandTemplate` is the template to use to render the cyclic schema expand message and controls
8+
*
9+
* @param props - The `CyclicSchemaExpandProps` for this component
10+
*/
11+
export default function CyclicSchemaExpandTemplate<
12+
T = any,
13+
S extends StrictRJSFSchema = RJSFSchema,
14+
F extends FormContextType = any,
15+
>(props: CyclicSchemaExpandProps<T, S, F>) {
16+
const { name, fieldPathId, registry, onExpand } = props;
17+
const { translateString } = registry;
18+
const buttonId = `${fieldPathId[ID_KEY]}-button`;
19+
return (
20+
<Box mt={4}>
21+
<Alert status='warning' title={translateString(TranslatableString.CycleDetected, [name])} mb={2} />
22+
<Button id={buttonId} size='sm' variant='outline' onClick={() => onExpand(fieldPathId[ID_KEY])}>
23+
{translateString(TranslatableString.ExpandButton)}
24+
</Button>
25+
</Box>
26+
);
27+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './CyclicSchemaExpandTemplate';
2+
export * from './CyclicSchemaExpandTemplate';

packages/chakra-ui/src/Templates/Templates.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import AddButton from '../AddButton';
44
import ArrayFieldItemTemplate from '../ArrayFieldItemTemplate';
55
import ArrayFieldTemplate from '../ArrayFieldTemplate';
66
import BaseInputTemplate from '../BaseInputTemplate/BaseInputTemplate';
7+
import CyclicSchemaExpandTemplate from '../CyclicSchemaExpandTemplate';
78
import DescriptionField from '../DescriptionField';
89
import ErrorList from '../ErrorList';
910
import FieldErrorTemplate from '../FieldErrorTemplate';
@@ -27,6 +28,7 @@ export function generateTemplates<
2728
ArrayFieldItemTemplate,
2829
ArrayFieldTemplate,
2930
BaseInputTemplate,
31+
CyclicSchemaExpandTemplate,
3032
ButtonTemplates: {
3133
CopyButton,
3234
AddButton,

0 commit comments

Comments
 (0)