From efbd6743cdb68b135b3df661b0545b9841d5c721 Mon Sep 17 00:00:00 2001 From: Robbie Culkin Date: Mon, 28 Jul 2025 15:31:56 -0700 Subject: [PATCH 1/5] works --- packages/core/src/components/Form.tsx | 27 ++- .../src/components/fields/SchemaField.tsx | 8 + packages/core/test/Form.test.jsx | 204 ++++++++++++++++++ packages/core/test/SchemaField.test.jsx | 2 + packages/utils/src/types.ts | 2 + 5 files changed, 241 insertions(+), 2 deletions(-) diff --git a/packages/core/src/components/Form.tsx b/packages/core/src/components/Form.tsx index b6b26bc8de..351e87ddfa 100644 --- a/packages/core/src/components/Form.tsx +++ b/packages/core/src/components/Form.tsx @@ -215,6 +215,17 @@ export interface FormProps>; @@ -514,9 +525,16 @@ export default class Form< * @returns - True if the component should be updated, false otherwise */ shouldComponentUpdate(nextProps: FormProps, nextState: FormState): boolean { + const { experimental_componentUpdateStrategy = 'customDeep' } = this.props; + + if (experimental_componentUpdateStrategy === 'default') { + // Use React's default behavior: always update if state or props change (no shouldComponentUpdate optimization) + return true; + } + + // Use custom deep equality checks via shouldRender return shouldRender(this, nextProps, nextState); } - /** Gets the previously raised customValidate errors. * * @returns the previous customValidate errors @@ -869,7 +887,11 @@ export default class Form< /** Returns the registry for the form */ getRegistry(): Registry { - const { translateString: customTranslateString, uiSchema = {} } = this.props; + const { + translateString: customTranslateString, + uiSchema = {}, + experimental_componentUpdateStrategy = 'customDeep', + } = this.props; const { schema, schemaUtils } = this.state; const { fields, templates, widgets, formContext, translateString } = getDefaultRegistry(); return { @@ -888,6 +910,7 @@ export default class Form< schemaUtils, translateString: customTranslateString || translateString, globalUiOptions: uiSchema[UI_GLOBAL_OPTIONS_KEY], + experimental_componentUpdateStrategy, }; } diff --git a/packages/core/src/components/fields/SchemaField.tsx b/packages/core/src/components/fields/SchemaField.tsx index 2b72e1c501..60cbce31f7 100644 --- a/packages/core/src/components/fields/SchemaField.tsx +++ b/packages/core/src/components/fields/SchemaField.tsx @@ -343,6 +343,14 @@ class SchemaField > { shouldComponentUpdate(nextProps: Readonly>) { + const { experimental_componentUpdateStrategy = 'customDeep' } = this.props.registry; + + if (experimental_componentUpdateStrategy === 'default') { + // Use React's default behavior: always update if state or props change (no shouldComponentUpdate optimization) + return true; + } + + // Use custom deep equality checks return !deepEquals(this.props, nextProps); } diff --git a/packages/core/test/Form.test.jsx b/packages/core/test/Form.test.jsx index 42623dc5fb..51d5ad6145 100644 --- a/packages/core/test/Form.test.jsx +++ b/packages/core/test/Form.test.jsx @@ -4972,4 +4972,208 @@ describe('Form omitExtraData and liveOmit', () => { expect(errors).to.have.lengthOf(0); }); }); + + describe('experimental_componentUpdateStrategy re-render behavior', () => { + let renderCount; + let TestField; + + beforeEach(() => { + renderCount = 0; + + // Create a custom field that tracks renders + TestField = class extends React.Component { + render() { + renderCount++; + return ; + } + }; + }); + + it('should prevent unnecessary re-renders with customDeep strategy when data unchanged', () => { + const schema = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }; + + const formData = { name: 'test' }; + + const { rerender } = createFormComponent({ + schema, + formData, + experimental_componentUpdateStrategy: 'customDeep', + fields: { StringField: TestField }, + }); + + const initialRenderCount = renderCount; + + // Re-render with same data - should not cause field re-render with customDeep + rerender({ + schema, + formData, + experimental_componentUpdateStrategy: 'customDeep', + fields: { StringField: TestField }, + }); + + // With customDeep strategy, render count should remain the same for identical data + expect(renderCount).to.equal(initialRenderCount); + }); + + it('should allow re-renders with default strategy even when data unchanged', () => { + const schema = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }; + + const formData = { name: 'test' }; + + const { rerender } = createFormComponent({ + schema, + formData, + experimental_componentUpdateStrategy: 'default', + fields: { StringField: TestField }, + }); + + const initialRenderCount = renderCount; + + // Re-render with same data - should cause field re-render with default strategy + rerender({ + schema, + formData, + experimental_componentUpdateStrategy: 'default', + fields: { StringField: TestField }, + }); + + // With default strategy, render count should increase even for identical data + expect(renderCount).to.be.greaterThan(initialRenderCount); + }); + + it('should re-render with customDeep strategy when data actually changes', () => { + const schema = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }; + + const initialFormData = { name: 'test' }; + const updatedFormData = { name: 'updated' }; + + const { rerender } = createFormComponent({ + schema, + formData: initialFormData, + experimental_componentUpdateStrategy: 'customDeep', + fields: { StringField: TestField }, + }); + + const initialRenderCount = renderCount; + + // Re-render with different data - should cause field re-render even with customDeep + rerender({ + schema, + formData: updatedFormData, + experimental_componentUpdateStrategy: 'customDeep', + fields: { StringField: TestField }, + }); + + // With customDeep strategy and changed data, render count should increase + expect(renderCount).to.be.greaterThan(initialRenderCount); + }); + + it('should handle nested object changes with customDeep strategy', () => { + const schema = { + type: 'object', + properties: { + user: { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'number' }, + }, + }, + }, + }; + + const initialFormData = { user: { name: 'John', age: 25 } }; + + const { rerender } = createFormComponent({ + schema, + formData: initialFormData, + experimental_componentUpdateStrategy: 'customDeep', + fields: { ObjectField: TestField }, + }); + + const initialRenderCount = renderCount; + + // Re-render with same nested data - should not cause re-render + rerender({ + schema, + formData: { user: { name: 'John', age: 25 } }, // Same values, different object reference + experimental_componentUpdateStrategy: 'customDeep', + fields: { ObjectField: TestField }, + }); + + // With customDeep strategy, should not re-render for deep-equal data + expect(renderCount).to.equal(initialRenderCount); + + // Now change nested data + rerender({ + schema, + formData: { user: { name: 'John', age: 26 } }, // Changed age + experimental_componentUpdateStrategy: 'customDeep', + fields: { ObjectField: TestField }, + }); + + // Should re-render when nested data actually changes + expect(renderCount).to.be.greaterThan(initialRenderCount); + }); + + it('should handle array changes with customDeep strategy', () => { + const schema = { + type: 'object', + properties: { + items: { + type: 'array', + items: { type: 'string' }, + }, + }, + }; + + const initialFormData = { items: ['a', 'b', 'c'] }; + + const { rerender } = createFormComponent({ + schema, + formData: initialFormData, + experimental_componentUpdateStrategy: 'customDeep', + fields: { ArrayField: TestField }, + }); + + const initialRenderCount = renderCount; + + // Re-render with same array data - should not cause re-render + rerender({ + schema, + formData: { items: ['a', 'b', 'c'] }, // Same values, different array reference + experimental_componentUpdateStrategy: 'customDeep', + fields: { ArrayField: TestField }, + }); + + // With customDeep strategy, should not re-render for deep-equal arrays + expect(renderCount).to.equal(initialRenderCount); + + // Now change array data + rerender({ + schema, + formData: { items: ['a', 'b', 'c', 'd'] }, // Added item + experimental_componentUpdateStrategy: 'customDeep', + fields: { ArrayField: TestField }, + }); + + // Should re-render when array data actually changes + expect(renderCount).to.be.greaterThan(initialRenderCount); + }); + }); }); diff --git a/packages/core/test/SchemaField.test.jsx b/packages/core/test/SchemaField.test.jsx index 3311fe783d..269ca81c9c 100644 --- a/packages/core/test/SchemaField.test.jsx +++ b/packages/core/test/SchemaField.test.jsx @@ -52,6 +52,7 @@ describe('SchemaField', () => { schemaUtils, translateString: englishStringTranslator, globalUiOptions: undefined, + experimental_componentUpdateStrategy: 'customDeep', }); }); it('should provide expected registry with globalUiOptions as prop', () => { @@ -86,6 +87,7 @@ describe('SchemaField', () => { schemaUtils, translateString: englishStringTranslator, globalUiOptions: { copyable: true }, + experimental_componentUpdateStrategy: 'customDeep', }); }); }); diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index 88b1997056..7b39133f99 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -423,6 +423,8 @@ export interface Registry string; /** The optional global UI Options that are available for all templates, fields and widgets to access */ globalUiOptions?: GlobalUISchemaOptions; + /** The component update strategy used by the Form and its fields for performance optimization */ + experimental_componentUpdateStrategy?: 'customDeep' | 'default'; } /** The properties that are passed to a Field implementation */ From 49808a50ad625dee99f86bcbcf74635c66edee59 Mon Sep 17 00:00:00 2001 From: Robbie Culkin Date: Mon, 28 Jul 2025 15:56:36 -0700 Subject: [PATCH 2/5] changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f64b8d014..23bb41c0f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,16 @@ should change the heading of the (upcoming) version to include a major version b --> +# 6.0.0-beta.13 + +## @rjsf/core + +- Added `experimental_componentUpdateStrategy` prop to `Form` component to control re-render optimization behavior. Supports `'customDeep'` (default, uses deep equality checks) and `'default'` (uses React's default behavior). The strategy is accessible from `SchemaField` components via the registry. + +## @rjsf/utils + +- Extended `Registry` interface to include optional `experimental_componentUpdateStrategy` property + # 6.0.0-beta.12 ## @rjsf/core From 3927aaa316de4583c46ff31e1eb13312f26716a7 Mon Sep 17 00:00:00 2001 From: Robbie Culkin Date: Mon, 28 Jul 2025 16:08:10 -0700 Subject: [PATCH 3/5] move param up in interface --- packages/core/src/components/Form.tsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/core/src/components/Form.tsx b/packages/core/src/components/Form.tsx index 351e87ddfa..b4f498751a 100644 --- a/packages/core/src/components/Form.tsx +++ b/packages/core/src/components/Form.tsx @@ -195,8 +195,19 @@ export interface FormProps; // Private /** @@ -215,17 +226,6 @@ export interface FormProps>; From 1de9ea64b0ea13c7adc948b86cb4908c508694dd Mon Sep 17 00:00:00 2001 From: Robbie Culkin Date: Mon, 28 Jul 2025 16:15:32 -0700 Subject: [PATCH 4/5] playground --- packages/playground/src/components/Header.tsx | 6 ++++++ packages/playground/src/components/Playground.tsx | 1 + 2 files changed, 7 insertions(+) diff --git a/packages/playground/src/components/Header.tsx b/packages/playground/src/components/Header.tsx index 350b5393dc..912239fe9c 100644 --- a/packages/playground/src/components/Header.tsx +++ b/packages/playground/src/components/Header.tsx @@ -70,6 +70,12 @@ const liveSettingsBooleanSchema: RJSFSchema = { noValidate: { type: 'boolean', title: 'Disable validation' }, noHtml5Validate: { type: 'boolean', title: 'Disable HTML 5 validation' }, focusOnFirstError: { type: 'boolean', title: 'Focus on 1st Error' }, + experimental_componentUpdateStrategy: { + type: 'string', + title: 'Component update strategy', + default: 'customDeep', + enum: ['customDeep', 'default'], + }, showErrorList: { type: 'string', default: 'top', diff --git a/packages/playground/src/components/Playground.tsx b/packages/playground/src/components/Playground.tsx index 3763c178ae..d258a42996 100644 --- a/packages/playground/src/components/Playground.tsx +++ b/packages/playground/src/components/Playground.tsx @@ -42,6 +42,7 @@ export default function Playground({ themes, validators }: PlaygroundProps) { readonly: false, omitExtraData: false, liveOmit: false, + experimental_componentUpdateStrategy: 'customDeep', experimental_defaultFormStateBehavior: { arrayMinItems: 'populate', emptyObjectFields: 'populateAllDefaults' }, }); const [otherFormProps, setOtherFormProps] = useState>({}); From 19e7c4c9f7feb44cdf15567842cd034878430785 Mon Sep 17 00:00:00 2001 From: Robbie Culkin Date: Mon, 28 Jul 2025 17:55:28 -0700 Subject: [PATCH 5/5] add shallow option. better separation of concerns in tests --- CHANGELOG.md | 3 +- packages/core/src/components/Form.tsx | 14 +- .../src/components/fields/SchemaField.tsx | 10 +- packages/core/test/Form.test.jsx | 204 ------------------ .../test/__snapshots__/Array.test.tsx.snap | 46 ++++ .../test/__snapshots__/Form.test.tsx.snap | 50 +++++ .../test/__snapshots__/Object.test.tsx.snap | 33 +++ packages/playground/src/components/Header.tsx | 2 +- packages/utils/src/index.ts | 4 + packages/utils/src/shallowEquals.ts | 41 ++++ packages/utils/src/shouldRender.ts | 29 ++- packages/utils/src/types.ts | 2 +- packages/utils/test/shallowEquals.test.ts | 95 ++++++++ packages/utils/test/shouldRender.test.tsx | 171 +++++++++++++-- 14 files changed, 454 insertions(+), 250 deletions(-) create mode 100644 packages/utils/src/shallowEquals.ts create mode 100644 packages/utils/test/shallowEquals.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 23bb41c0f3..f315d0bb3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,11 +20,12 @@ should change the heading of the (upcoming) version to include a major version b ## @rjsf/core -- Added `experimental_componentUpdateStrategy` prop to `Form` component to control re-render optimization behavior. Supports `'customDeep'` (default, uses deep equality checks) and `'default'` (uses React's default behavior). The strategy is accessible from `SchemaField` components via the registry. +- Added `experimental_componentUpdateStrategy` prop to `Form` component to control re-render optimization behavior. Supports `'customDeep'` (default, uses deep equality checks that ignore functions), `'shallow'`, and `'always'` ## @rjsf/utils - Extended `Registry` interface to include optional `experimental_componentUpdateStrategy` property +- Added `shallowEquals()` utility function for shallow equality comparisons # 6.0.0-beta.12 diff --git a/packages/core/src/components/Form.tsx b/packages/core/src/components/Form.tsx index b4f498751a..05d9509987 100644 --- a/packages/core/src/components/Form.tsx +++ b/packages/core/src/components/Form.tsx @@ -200,11 +200,12 @@ export interface FormProps, nextState: FormState): boolean { const { experimental_componentUpdateStrategy = 'customDeep' } = this.props; - - if (experimental_componentUpdateStrategy === 'default') { - // Use React's default behavior: always update if state or props change (no shouldComponentUpdate optimization) - return true; - } - - // Use custom deep equality checks via shouldRender - return shouldRender(this, nextProps, nextState); + return shouldRender(this, nextProps, nextState, experimental_componentUpdateStrategy); } /** Gets the previously raised customValidate errors. * diff --git a/packages/core/src/components/fields/SchemaField.tsx b/packages/core/src/components/fields/SchemaField.tsx index 60cbce31f7..85846200a6 100644 --- a/packages/core/src/components/fields/SchemaField.tsx +++ b/packages/core/src/components/fields/SchemaField.tsx @@ -1,7 +1,6 @@ import { useCallback, Component, ComponentType } from 'react'; import { ADDITIONAL_PROPERTY_FLAG, - deepEquals, descriptionId, ErrorSchema, FieldProps, @@ -15,6 +14,7 @@ import { mergeObjects, Registry, RJSFSchema, + shouldRender, StrictRJSFSchema, TranslatableString, UI_OPTIONS_KEY, @@ -345,13 +345,7 @@ class SchemaField>) { const { experimental_componentUpdateStrategy = 'customDeep' } = this.props.registry; - if (experimental_componentUpdateStrategy === 'default') { - // Use React's default behavior: always update if state or props change (no shouldComponentUpdate optimization) - return true; - } - - // Use custom deep equality checks - return !deepEquals(this.props, nextProps); + return shouldRender(this, nextProps, this.state, experimental_componentUpdateStrategy); } render() { diff --git a/packages/core/test/Form.test.jsx b/packages/core/test/Form.test.jsx index 51d5ad6145..42623dc5fb 100644 --- a/packages/core/test/Form.test.jsx +++ b/packages/core/test/Form.test.jsx @@ -4972,208 +4972,4 @@ describe('Form omitExtraData and liveOmit', () => { expect(errors).to.have.lengthOf(0); }); }); - - describe('experimental_componentUpdateStrategy re-render behavior', () => { - let renderCount; - let TestField; - - beforeEach(() => { - renderCount = 0; - - // Create a custom field that tracks renders - TestField = class extends React.Component { - render() { - renderCount++; - return ; - } - }; - }); - - it('should prevent unnecessary re-renders with customDeep strategy when data unchanged', () => { - const schema = { - type: 'object', - properties: { - name: { type: 'string' }, - }, - }; - - const formData = { name: 'test' }; - - const { rerender } = createFormComponent({ - schema, - formData, - experimental_componentUpdateStrategy: 'customDeep', - fields: { StringField: TestField }, - }); - - const initialRenderCount = renderCount; - - // Re-render with same data - should not cause field re-render with customDeep - rerender({ - schema, - formData, - experimental_componentUpdateStrategy: 'customDeep', - fields: { StringField: TestField }, - }); - - // With customDeep strategy, render count should remain the same for identical data - expect(renderCount).to.equal(initialRenderCount); - }); - - it('should allow re-renders with default strategy even when data unchanged', () => { - const schema = { - type: 'object', - properties: { - name: { type: 'string' }, - }, - }; - - const formData = { name: 'test' }; - - const { rerender } = createFormComponent({ - schema, - formData, - experimental_componentUpdateStrategy: 'default', - fields: { StringField: TestField }, - }); - - const initialRenderCount = renderCount; - - // Re-render with same data - should cause field re-render with default strategy - rerender({ - schema, - formData, - experimental_componentUpdateStrategy: 'default', - fields: { StringField: TestField }, - }); - - // With default strategy, render count should increase even for identical data - expect(renderCount).to.be.greaterThan(initialRenderCount); - }); - - it('should re-render with customDeep strategy when data actually changes', () => { - const schema = { - type: 'object', - properties: { - name: { type: 'string' }, - }, - }; - - const initialFormData = { name: 'test' }; - const updatedFormData = { name: 'updated' }; - - const { rerender } = createFormComponent({ - schema, - formData: initialFormData, - experimental_componentUpdateStrategy: 'customDeep', - fields: { StringField: TestField }, - }); - - const initialRenderCount = renderCount; - - // Re-render with different data - should cause field re-render even with customDeep - rerender({ - schema, - formData: updatedFormData, - experimental_componentUpdateStrategy: 'customDeep', - fields: { StringField: TestField }, - }); - - // With customDeep strategy and changed data, render count should increase - expect(renderCount).to.be.greaterThan(initialRenderCount); - }); - - it('should handle nested object changes with customDeep strategy', () => { - const schema = { - type: 'object', - properties: { - user: { - type: 'object', - properties: { - name: { type: 'string' }, - age: { type: 'number' }, - }, - }, - }, - }; - - const initialFormData = { user: { name: 'John', age: 25 } }; - - const { rerender } = createFormComponent({ - schema, - formData: initialFormData, - experimental_componentUpdateStrategy: 'customDeep', - fields: { ObjectField: TestField }, - }); - - const initialRenderCount = renderCount; - - // Re-render with same nested data - should not cause re-render - rerender({ - schema, - formData: { user: { name: 'John', age: 25 } }, // Same values, different object reference - experimental_componentUpdateStrategy: 'customDeep', - fields: { ObjectField: TestField }, - }); - - // With customDeep strategy, should not re-render for deep-equal data - expect(renderCount).to.equal(initialRenderCount); - - // Now change nested data - rerender({ - schema, - formData: { user: { name: 'John', age: 26 } }, // Changed age - experimental_componentUpdateStrategy: 'customDeep', - fields: { ObjectField: TestField }, - }); - - // Should re-render when nested data actually changes - expect(renderCount).to.be.greaterThan(initialRenderCount); - }); - - it('should handle array changes with customDeep strategy', () => { - const schema = { - type: 'object', - properties: { - items: { - type: 'array', - items: { type: 'string' }, - }, - }, - }; - - const initialFormData = { items: ['a', 'b', 'c'] }; - - const { rerender } = createFormComponent({ - schema, - formData: initialFormData, - experimental_componentUpdateStrategy: 'customDeep', - fields: { ArrayField: TestField }, - }); - - const initialRenderCount = renderCount; - - // Re-render with same array data - should not cause re-render - rerender({ - schema, - formData: { items: ['a', 'b', 'c'] }, // Same values, different array reference - experimental_componentUpdateStrategy: 'customDeep', - fields: { ArrayField: TestField }, - }); - - // With customDeep strategy, should not re-render for deep-equal arrays - expect(renderCount).to.equal(initialRenderCount); - - // Now change array data - rerender({ - schema, - formData: { items: ['a', 'b', 'c', 'd'] }, // Added item - experimental_componentUpdateStrategy: 'customDeep', - fields: { ArrayField: TestField }, - }); - - // Should re-render when array data actually changes - expect(renderCount).to.be.greaterThan(initialRenderCount); - }); - }); }); diff --git a/packages/daisyui/test/__snapshots__/Array.test.tsx.snap b/packages/daisyui/test/__snapshots__/Array.test.tsx.snap index d8a068ccb6..abd9864b38 100644 --- a/packages/daisyui/test/__snapshots__/Array.test.tsx.snap +++ b/packages/daisyui/test/__snapshots__/Array.test.tsx.snap @@ -14,6 +14,7 @@ exports[`array fields array 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -3681,6 +3682,7 @@ exports[`array fields array icons 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -7279,6 +7281,7 @@ exports[`array fields array icons 1`] = ` id="root_0__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -11034,6 +11037,7 @@ exports[`array fields array icons 1`] = ` id="root_1__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -14859,6 +14863,7 @@ exports[`array fields checkboxes 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -18589,6 +18594,7 @@ exports[`array fields empty errors array 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -22184,6 +22190,7 @@ exports[`array fields empty errors array 1`] = ` id="root_name__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -25855,6 +25862,7 @@ exports[`array fields fixed array 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -29462,6 +29470,7 @@ exports[`array fields fixed array 1`] = ` id="root_0__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -33112,6 +33121,7 @@ exports[`array fields fixed array 1`] = ` id="root_1__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -36805,6 +36815,7 @@ exports[`array fields has errors 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -40400,6 +40411,7 @@ exports[`array fields has errors 1`] = ` id="root_name__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -44075,6 +44087,7 @@ exports[`array fields no errors 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -47670,6 +47683,7 @@ exports[`array fields no errors 1`] = ` id="root_name__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -51341,6 +51355,7 @@ exports[`with title and description array 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -55043,6 +55058,7 @@ exports[`with title and description array icons 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -58676,6 +58692,7 @@ exports[`with title and description array icons 1`] = ` id="root_0__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -62444,6 +62461,7 @@ exports[`with title and description array icons 1`] = ` id="root_1__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -66282,6 +66300,7 @@ exports[`with title and description checkboxes 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -70026,6 +70045,7 @@ exports[`with title and description fixed array 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -73674,6 +73694,7 @@ exports[`with title and description fixed array 1`] = ` id="root_0__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -77341,6 +77362,7 @@ exports[`with title and description fixed array 1`] = ` id="root_1__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -81040,6 +81062,7 @@ exports[`with title and description from both array 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -84749,6 +84772,7 @@ exports[`with title and description from both array icons 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -88384,6 +88408,7 @@ exports[`with title and description from both array icons 1`] = ` id="root_0__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -92154,6 +92179,7 @@ exports[`with title and description from both array icons 1`] = ` id="root_1__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -95994,6 +96020,7 @@ exports[`with title and description from both checkboxes 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -99745,6 +99772,7 @@ exports[`with title and description from both fixed array 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -103394,6 +103422,7 @@ exports[`with title and description from both fixed array 1`] = ` id="root_0__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -107058,6 +107087,7 @@ exports[`with title and description from both fixed array 1`] = ` id="root_1__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -110754,6 +110784,7 @@ exports[`with title and description from uiSchema array 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -114457,6 +114488,7 @@ exports[`with title and description from uiSchema array icons 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -118086,6 +118118,7 @@ exports[`with title and description from uiSchema array icons 1`] = ` id="root_0__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -121852,6 +121885,7 @@ exports[`with title and description from uiSchema array icons 1`] = ` id="root_1__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -125688,6 +125722,7 @@ exports[`with title and description from uiSchema checkboxes 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -129433,6 +129468,7 @@ exports[`with title and description from uiSchema fixed array 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -133076,6 +133112,7 @@ exports[`with title and description from uiSchema fixed array 1`] = ` id="root_0__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -136736,6 +136773,7 @@ exports[`with title and description from uiSchema fixed array 1`] = ` id="root_1__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -140428,6 +140466,7 @@ exports[`with title and description with global label off array 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -144115,6 +144154,7 @@ exports[`with title and description with global label off array icons 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -147730,6 +147770,7 @@ exports[`with title and description with global label off array icons 1`] = ` id="root_0__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -151485,6 +151526,7 @@ exports[`with title and description with global label off array icons 1`] = ` id="root_1__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -155310,6 +155352,7 @@ exports[`with title and description with global label off checkboxes 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -159062,6 +159105,7 @@ exports[`with title and description with global label off fixed array 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -162695,6 +162739,7 @@ exports[`with title and description with global label off fixed array 1`] = ` id="root_0__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -166349,6 +166394,7 @@ exports[`with title and description with global label off fixed array 1`] = ` id="root_1__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], diff --git a/packages/daisyui/test/__snapshots__/Form.test.tsx.snap b/packages/daisyui/test/__snapshots__/Form.test.tsx.snap index 19bbdec510..55bcbfd6b5 100644 --- a/packages/daisyui/test/__snapshots__/Form.test.tsx.snap +++ b/packages/daisyui/test/__snapshots__/Form.test.tsx.snap @@ -14,6 +14,7 @@ exports[`single fields checkbox field 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -3629,6 +3630,7 @@ exports[`single fields checkbox field with label 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -7260,6 +7262,7 @@ exports[`single fields checkbox field with label and description 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -10904,6 +10907,7 @@ exports[`single fields checkbox field with label and rich text description 1`] = id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -14560,6 +14564,7 @@ exports[`single fields checkboxes field 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -18305,6 +18310,7 @@ exports[`single fields field with description 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -21903,6 +21909,7 @@ exports[`single fields field with description 1`] = ` id="root_my-field__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -25577,6 +25584,7 @@ exports[`single fields field with description in uiSchema 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -29181,6 +29189,7 @@ exports[`single fields field with description in uiSchema 1`] = ` id="root_my-field__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -32860,6 +32869,7 @@ exports[`single fields field with markdown description 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -36464,6 +36474,7 @@ exports[`single fields field with markdown description 1`] = ` id="root_my-field__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -40143,6 +40154,7 @@ exports[`single fields field with markdown description in uiSchema 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -43748,6 +43760,7 @@ exports[`single fields field with markdown description in uiSchema 1`] = ` id="root_my-field__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -47428,6 +47441,7 @@ exports[`single fields format color 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -51072,6 +51086,7 @@ exports[`single fields format date 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -54727,6 +54742,7 @@ exports[`single fields format datetime 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -58382,6 +58398,7 @@ exports[`single fields format time 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -62021,6 +62038,7 @@ exports[`single fields help and error display 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -65672,6 +65690,7 @@ exports[`single fields hidden field 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -69270,6 +69289,7 @@ exports[`single fields hidden field 1`] = ` id="root_my-field__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -72917,6 +72937,7 @@ exports[`single fields hidden label 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -76556,6 +76577,7 @@ exports[`single fields null field 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -80166,6 +80188,7 @@ exports[`single fields number field 0 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -83808,6 +83831,7 @@ exports[`single fields number field 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -87450,6 +87474,7 @@ exports[`single fields password field 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -91095,6 +91120,7 @@ exports[`single fields radio field 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -94753,6 +94779,7 @@ exports[`single fields schema examples 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -98435,6 +98462,7 @@ exports[`single fields select field 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -102115,6 +102143,7 @@ exports[`single fields select field multiple choice 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -105869,6 +105898,7 @@ exports[`single fields select field multiple choice enumDisabled 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -109629,6 +109659,7 @@ exports[`single fields select field multiple choice enumDisabled using checkboxe id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -113372,6 +113403,7 @@ exports[`single fields select field multiple choice formData 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -117126,6 +117158,7 @@ exports[`single fields select field multiple choice with labels 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -120901,6 +120934,7 @@ exports[`single fields select field single choice enumDisabled 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -124587,6 +124621,7 @@ exports[`single fields select field single choice enumDisabled using radio widge id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -128268,6 +128303,7 @@ exports[`single fields select field single choice form disabled using radio widg id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -131946,6 +131982,7 @@ exports[`single fields select field single choice formData 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -135628,6 +135665,7 @@ exports[`single fields select field single choice uiSchema disabled using radio id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -139307,6 +139345,7 @@ exports[`single fields slider field 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -142953,6 +142992,7 @@ exports[`single fields string field format data-url 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -146576,6 +146616,7 @@ exports[`single fields string field format email 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -150220,6 +150261,7 @@ exports[`single fields string field format uri 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -153864,6 +153906,7 @@ exports[`single fields string field regular 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -157505,6 +157548,7 @@ exports[`single fields string field with placeholder 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -161150,6 +161194,7 @@ exports[`single fields textarea field 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -164779,6 +164824,7 @@ exports[`single fields title field 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -168394,6 +168440,7 @@ exports[`single fields title field 1`] = ` id="root_title__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -172070,6 +172117,7 @@ exports[`single fields unsupported field 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -175700,6 +175748,7 @@ exports[`single fields up/down field 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -179345,6 +179394,7 @@ exports[`single fields using custom tagName 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], diff --git a/packages/daisyui/test/__snapshots__/Object.test.tsx.snap b/packages/daisyui/test/__snapshots__/Object.test.tsx.snap index 9a0805392c..228ec3eeac 100644 --- a/packages/daisyui/test/__snapshots__/Object.test.tsx.snap +++ b/packages/daisyui/test/__snapshots__/Object.test.tsx.snap @@ -14,6 +14,7 @@ exports[`object fields additionalProperties 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -3603,6 +3604,7 @@ exports[`object fields additionalProperties 1`] = ` id="root_foo__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -7306,6 +7308,7 @@ exports[`object fields object 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -10916,6 +10919,7 @@ exports[`object fields object 1`] = ` id="root_a__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -14568,6 +14572,7 @@ exports[`object fields object 1`] = ` id="root_b__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -18251,6 +18256,7 @@ exports[`object fields show add button and fields if additionalProperties is tru id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -21837,6 +21843,7 @@ exports[`object fields show add button and fields if additionalProperties is tru id="root_additionalProperty__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -25538,6 +25545,7 @@ exports[`object fields with title and description additionalProperties 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -29156,6 +29164,7 @@ exports[`object fields with title and description additionalProperties 1`] = ` id="root_foo__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -32863,6 +32872,7 @@ exports[`object fields with title and description from both additionalProperties id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -36500,6 +36510,7 @@ exports[`object fields with title and description from both additionalProperties id="root_foo__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -40207,6 +40218,7 @@ exports[`object fields with title and description from both object 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -43871,6 +43883,7 @@ exports[`object fields with title and description from both object 1`] = ` id="root_a__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -47540,6 +47553,7 @@ exports[`object fields with title and description from both object 1`] = ` id="root_b__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -51240,6 +51254,7 @@ exports[`object fields with title and description from uiSchema additionalProper id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -54871,6 +54886,7 @@ exports[`object fields with title and description from uiSchema additionalProper id="root_foo__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -58574,6 +58590,7 @@ exports[`object fields with title and description from uiSchema object 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -62226,6 +62243,7 @@ exports[`object fields with title and description from uiSchema object 1`] = ` id="root_a__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -65886,6 +65904,7 @@ exports[`object fields with title and description from uiSchema object 1`] = ` id="root_b__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -69577,6 +69596,7 @@ exports[`object fields with title and description from uiSchema show add button id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -73205,6 +73225,7 @@ exports[`object fields with title and description from uiSchema show add button id="root_additionalProperty__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -76906,6 +76927,7 @@ exports[`object fields with title and description object 1`] = ` id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -80551,6 +80573,7 @@ exports[`object fields with title and description object 1`] = ` id="root_a__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -84212,6 +84235,7 @@ exports[`object fields with title and description object 1`] = ` id="root_b__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -87904,6 +87928,7 @@ exports[`object fields with title and description show add button and fields if id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -91519,6 +91544,7 @@ exports[`object fields with title and description show add button and fields if id="root_additionalProperty__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -95224,6 +95250,7 @@ exports[`object fields with title and description with global label off addition id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -98827,6 +98854,7 @@ exports[`object fields with title and description with global label off addition id="root_foo__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -102526,6 +102554,7 @@ exports[`object fields with title and description with global label off object 1 id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -106156,6 +106185,7 @@ exports[`object fields with title and description with global label off object 1 id="root_a__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -109809,6 +109839,7 @@ exports[`object fields with title and description with global label off object 1 id="root_b__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -113493,6 +113524,7 @@ exports[`object fields with title and description with global label off show add id="root__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], @@ -117093,6 +117125,7 @@ exports[`object fields with title and description with global label off show add id="root_additionalProperty__description" registry={ { + "experimental_componentUpdateStrategy": "customDeep", "fields": { "AnyOfField": [Function], "ArrayField": [Function], diff --git a/packages/playground/src/components/Header.tsx b/packages/playground/src/components/Header.tsx index 912239fe9c..e6ab277cd1 100644 --- a/packages/playground/src/components/Header.tsx +++ b/packages/playground/src/components/Header.tsx @@ -74,7 +74,7 @@ const liveSettingsBooleanSchema: RJSFSchema = { type: 'string', title: 'Component update strategy', default: 'customDeep', - enum: ['customDeep', 'default'], + enum: ['customDeep', 'shallow', 'always'], }, showErrorList: { type: 'string', diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 8d2029cebc..de875d2ff6 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -6,6 +6,7 @@ import createSchemaUtils from './createSchemaUtils'; import dataURItoBlob from './dataURItoBlob'; import dateRangeOptions from './dateRangeOptions'; import deepEquals from './deepEquals'; +import shallowEquals from './shallowEquals'; import englishStringTranslator from './englishStringTranslator'; import enumOptionsDeselectValue from './enumOptionsDeselectValue'; import enumOptionsIndexForValue from './enumOptionsIndexForValue'; @@ -130,6 +131,7 @@ export { rangeSpec, replaceStringParameters, schemaRequiresTrueValue, + shallowEquals, shouldRender, sortedJSONStringify, titleId, @@ -142,3 +144,5 @@ export { validationDataMerge, withIdRefPrefix, }; + +export type { ComponentUpdateStrategy } from './shouldRender'; diff --git a/packages/utils/src/shallowEquals.ts b/packages/utils/src/shallowEquals.ts new file mode 100644 index 0000000000..844c793823 --- /dev/null +++ b/packages/utils/src/shallowEquals.ts @@ -0,0 +1,41 @@ +/** Implements a shallow equals comparison that uses Object.is() for comparing values. + * This function compares objects by checking if all keys and their values are equal using Object.is(). + * + * @param a - The first element to compare + * @param b - The second element to compare + * @returns - True if the `a` and `b` are shallow equal, false otherwise + */ +export default function shallowEquals(a: any, b: any): boolean { + // If they're the same reference, they're equal + if (Object.is(a, b)) { + return true; + } + + // If either is null or undefined, they're not equal (since we know they're not the same reference) + if (a == null || b == null) { + return false; + } + + // If they're not objects, they're not equal (since Object.is already checked) + if (typeof a !== 'object' || typeof b !== 'object') { + return false; + } + + const keysA = Object.keys(a); + const keysB = Object.keys(b); + + // Different number of keys means not equal + if (keysA.length !== keysB.length) { + return false; + } + + // Check if all keys and values are equal + for (let i = 0; i < keysA.length; i++) { + const key = keysA[i]; + if (!Object.prototype.hasOwnProperty.call(b, key) || !Object.is(a[key], b[key])) { + return false; + } + } + + return true; +} diff --git a/packages/utils/src/shouldRender.ts b/packages/utils/src/shouldRender.ts index e9532447da..3de5bc5cc1 100644 --- a/packages/utils/src/shouldRender.ts +++ b/packages/utils/src/shouldRender.ts @@ -1,16 +1,41 @@ import React from 'react'; import deepEquals from './deepEquals'; +import shallowEquals from './shallowEquals'; + +/** The supported component update strategies */ +export type ComponentUpdateStrategy = 'customDeep' | 'shallow' | 'always'; /** Determines whether the given `component` should be rerendered by comparing its current set of props and state - * against the next set. If either of those two sets are not the same, then the component should be rerendered. + * against the next set. The comparison strategy can be controlled via the `updateStrategy` parameter. * * @param component - A React component being checked * @param nextProps - The next set of props against which to check * @param nextState - The next set of state against which to check + * @param updateStrategy - The strategy to use for comparison: + * - 'customDeep': Uses RJSF's custom deep equality checks (default) + * - 'shallow': Uses shallow comparison of props and state + * - 'always': Always returns true (React's default behavior) * @returns - True if the component should be re-rendered, false otherwise */ -export default function shouldRender(component: React.Component, nextProps: any, nextState: any) { +export default function shouldRender( + component: React.Component, + nextProps: any, + nextState: any, + updateStrategy: ComponentUpdateStrategy = 'customDeep', +) { + if (updateStrategy === 'always') { + // Use React's default behavior: always update if state or props change (no shouldComponentUpdate optimization) + return true; + } + + if (updateStrategy === 'shallow') { + // Use shallow comparison for props and state + const { props, state } = component; + return !shallowEquals(props, nextProps) || !shallowEquals(state, nextState); + } + + // Use custom deep equality checks (default 'customDeep' strategy) const { props, state } = component; return !deepEquals(props, nextProps) || !deepEquals(state, nextState); } diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index 7b39133f99..411bc741c7 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -424,7 +424,7 @@ export interface Registry { + it('should return true for identical references', () => { + const obj = { a: 1 }; + expect(shallowEquals(obj, obj)).toBe(true); + }); + + it('should return true for primitive values using Object.is', () => { + expect(shallowEquals(1, 1)).toBe(true); + expect(shallowEquals('test', 'test')).toBe(true); + expect(shallowEquals(true, true)).toBe(true); + expect(shallowEquals(null, null)).toBe(true); + expect(shallowEquals(undefined, undefined)).toBe(true); + }); + + it('should return false for different primitive values', () => { + expect(shallowEquals(1, 2)).toBe(false); + expect(shallowEquals('test', 'other')).toBe(false); + expect(shallowEquals(true, false)).toBe(false); + }); + + it('should handle null and undefined values', () => { + expect(shallowEquals(null, undefined)).toBe(false); + expect(shallowEquals(null, {})).toBe(false); + expect(shallowEquals(undefined, {})).toBe(false); + expect(shallowEquals({}, null)).toBe(false); + expect(shallowEquals({}, undefined)).toBe(false); + }); + + it('should return false for objects with different number of keys', () => { + expect(shallowEquals({ a: 1 }, { a: 1, b: 2 })).toBe(false); + expect(shallowEquals({ a: 1, b: 2 }, { a: 1 })).toBe(false); + }); + + it('should return true for objects with same keys and shallow equal values', () => { + expect(shallowEquals({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true); + expect(shallowEquals({ a: 'test', b: true }, { a: 'test', b: true })).toBe(true); + }); + + it('should return false for objects with different values', () => { + expect(shallowEquals({ a: 1 }, { a: 2 })).toBe(false); + expect(shallowEquals({ a: 1, b: 2 }, { a: 1, b: 3 })).toBe(false); + }); + + it('should handle function references correctly with Object.is', () => { + const fn1 = () => {}; + const fn2 = () => {}; + + // Same function reference should be equal + expect(shallowEquals({ fn: fn1 }, { fn: fn1 })).toBe(true); + + // Different function references should not be equal + expect(shallowEquals({ fn: fn1 }, { fn: fn2 })).toBe(false); + }); + + it('should return false for nested object changes (shallow comparison)', () => { + const obj1 = { nested: { value: 1 } }; + const obj2 = { nested: { value: 1 } }; // Different reference but same content + + // Different object references should not be equal with shallow comparison + expect(shallowEquals(obj1, obj2)).toBe(false); + + // Same object reference should be equal + expect(shallowEquals(obj1, obj1)).toBe(true); + }); + + it('should handle missing keys correctly', () => { + expect(shallowEquals({ a: 1 }, {})).toBe(false); + expect(shallowEquals({}, { a: 1 })).toBe(false); + }); + + it('should handle objects with hasOwnProperty correctly', () => { + const obj1 = { a: 1, hasOwnProperty: 'custom' }; + const obj2 = { a: 1, hasOwnProperty: 'custom' }; + expect(shallowEquals(obj1, obj2)).toBe(true); + }); + + it('should return false for non-object types when not identical', () => { + expect(shallowEquals('string', 123)).toBe(false); + expect(shallowEquals(true, 'true')).toBe(false); + expect(shallowEquals([1], { a: 1 })).toBe(false); // Different key structure + }); + + it('should handle arrays as objects', () => { + expect(shallowEquals([1, 2], [1, 2])).toBe(true); + expect(shallowEquals([1, 2], [1, 3])).toBe(false); + expect(shallowEquals([1, 2], [1, 2, 3])).toBe(false); + }); + + it('should handle empty objects', () => { + expect(shallowEquals({}, {})).toBe(true); + expect(shallowEquals([], [])).toBe(true); + }); +}); diff --git a/packages/utils/test/shouldRender.test.tsx b/packages/utils/test/shouldRender.test.tsx index 8b5970f560..e4fd873f22 100644 --- a/packages/utils/test/shouldRender.test.tsx +++ b/packages/utils/test/shouldRender.test.tsx @@ -21,7 +21,71 @@ type MyComponentState = { }; describe('shouldRender()', () => { - describe('single level comparison checks', () => { + describe('default strategy (customDeep)', () => { + describe('single level comparison checks', () => { + let initial: React.Component; + beforeAll(() => { + initial = { + props: { myProp: 1 }, + state: { myState: 1 }, + } as React.Component; + }); + + it('should detect equivalent props and state', () => { + expect(shouldRender(initial, { myProp: 1 }, { myState: 1 })).toBe(false); + }); + + it('should detect diffing props', () => { + expect(shouldRender(initial, { myProp: 2 }, { myState: 1 })).toBe(true); + }); + + it('should detect diffing state', () => { + expect(shouldRender(initial, { myProp: 1 }, { myState: 2 })).toBe(true); + }); + + it('should handle equivalent function prop', () => { + const fn = () => {}; + initial = { + props: { myProp: fn }, + state: { myState: 1 }, + } as React.Component; + expect(shouldRender(initial, { myProp: fn }, { myState: 1 })).toBe(false); + }); + }); + + describe('nested levels comparison checks', () => { + let initial: React.Component; + beforeAll(() => { + initial = { + props: { myProp: { mySubProp: 1 } }, + state: { myState: { mySubState: 1 } }, + } as React.Component; + }); + + it('should detect equivalent props and state', () => { + expect(shouldRender(initial, { myProp: { mySubProp: 1 } }, { myState: { mySubState: 1 } })).toBe(false); + }); + + it('should detect diffing props', () => { + expect(shouldRender(initial, { myProp: { mySubProp: 2 } }, { myState: { mySubState: 1 } })).toBe(true); + }); + + it('should detect diffing state', () => { + expect(shouldRender(initial, { myProp: { mySubProp: 1 } }, { myState: { mySubState: 2 } })).toBe(true); + }); + + it('should handle equivalent function prop', () => { + const fn = () => {}; + initial = { + props: { myProp: { mySubProp: fn } }, + state: { myState: { mySubState: fn } }, + } as React.Component; + expect(shouldRender(initial, { myProp: { mySubProp: fn } }, { myState: { mySubState: fn } })).toBe(false); + }); + }); + }); + + describe('always strategy (explicit)', () => { let initial: React.Component; beforeAll(() => { initial = { @@ -30,29 +94,54 @@ describe('shouldRender()', () => { } as React.Component; }); - it('should detect equivalent props and state', () => { - expect(shouldRender(initial, { myProp: 1 }, { myState: 1 })).toBe(false); + it('should return true even with identical props and state', () => { + expect(shouldRender(initial, initial.props, initial.state, 'always')).toBe(true); }); - it('should detect diffing props', () => { - expect(shouldRender(initial, { myProp: 2 }, { myState: 1 })).toBe(true); + it('should return true with different props', () => { + expect(shouldRender(initial, { myProp: 2 }, { myState: 1 }, 'always')).toBe(true); }); - it('should detect diffing state', () => { - expect(shouldRender(initial, { myProp: 1 }, { myState: 2 })).toBe(true); + it('should return true with different state', () => { + expect(shouldRender(initial, { myProp: 1 }, { myState: 2 }, 'always')).toBe(true); }); + }); - it('should handle equivalent function prop', () => { - const fn = () => {}; + describe('shallow strategy', () => { + let initial: React.Component; + beforeAll(() => { initial = { - props: { myProp: fn }, + props: { myProp: 1 }, state: { myState: 1 }, } as React.Component; - expect(shouldRender(initial, { myProp: fn }, { myState: 1 })).toBe(false); + }); + + it('should use shallow comparison for props and state', () => { + // Should not re-render when props and state are shallow equal + expect(shouldRender(initial, { myProp: 1 }, { myState: 1 }, 'shallow')).toBe(false); + + // Should re-render when props differ + expect(shouldRender(initial, { myProp: 2 }, { myState: 1 }, 'shallow')).toBe(true); + + // Should re-render when state differs + expect(shouldRender(initial, { myProp: 1 }, { myState: 2 }, 'shallow')).toBe(true); + }); + + it('should re-render for nested object changes (shallow comparison)', () => { + const obj1 = { mySubProp: 1 }; + const obj2 = { mySubProp: 1 }; // Different reference but same content + + initial = { + props: { myProp: obj1 }, + state: { myState: 1 }, + } as React.Component; + + // Different object references should trigger re-render with shallow comparison + expect(shouldRender(initial, { myProp: obj2 }, { myState: 1 }, 'shallow')).toBe(true); }); }); - describe('nested levels comparison checks', () => { + describe('customDeep strategy (explicit)', () => { let initial: React.Component; beforeAll(() => { initial = { @@ -61,25 +150,61 @@ describe('shouldRender()', () => { } as React.Component; }); - it('should detect equivalent props and state', () => { - expect(shouldRender(initial, { myProp: { mySubProp: 1 } }, { myState: { mySubState: 1 } })).toBe(false); + it('should perform deep equality check', () => { + // Same structure, different references - should not re-render + expect(shouldRender(initial, { myProp: { mySubProp: 1 } }, { myState: { mySubState: 1 } }, 'customDeep')).toBe( + false, + ); }); - it('should detect diffing props', () => { - expect(shouldRender(initial, { myProp: { mySubProp: 2 } }, { myState: { mySubState: 1 } })).toBe(true); + it('should detect deep changes in props', () => { + expect(shouldRender(initial, { myProp: { mySubProp: 2 } }, { myState: { mySubState: 1 } }, 'customDeep')).toBe( + true, + ); }); - it('should detect diffing state', () => { - expect(shouldRender(initial, { myProp: { mySubProp: 1 } }, { myState: { mySubState: 2 } })).toBe(true); + it('should detect deep changes in state', () => { + expect(shouldRender(initial, { myProp: { mySubProp: 1 } }, { myState: { mySubState: 2 } }, 'customDeep')).toBe( + true, + ); }); - it('should handle equivalent function prop', () => { - const fn = () => {}; + it('should handle arrays correctly', () => { + const initialWithArray = { + props: { myProp: [1, 2, 3] }, + state: { myState: ['a', 'b'] }, + } as unknown as React.Component; + + // Same array content, different references + expect(shouldRender(initialWithArray, { myProp: [1, 2, 3] }, { myState: ['a', 'b'] }, 'customDeep')).toBe(false); + + // Different array content + expect(shouldRender(initialWithArray, { myProp: [1, 2, 4] }, { myState: ['a', 'b'] }, 'customDeep')).toBe(true); + }); + }); + + describe('edge cases and error handling', () => { + let initial: React.Component; + beforeAll(() => { initial = { - props: { myProp: { mySubProp: fn } }, - state: { myState: { mySubState: fn } }, + props: { myProp: 1 }, + state: { myState: 1 }, } as React.Component; - expect(shouldRender(initial, { myProp: { mySubProp: fn } }, { myState: { mySubState: fn } })).toBe(false); + }); + + it('should default to customDeep when no strategy provided', () => { + const initialNested = { + props: { myProp: { mySubProp: 1 } }, + state: { myState: { mySubState: 1 } }, + } as React.Component; + + // Should behave like customDeep (default) + expect(shouldRender(initialNested, { myProp: { mySubProp: 1 } }, { myState: { mySubState: 1 } })).toBe(false); + }); + + it('should handle invalid strategy gracefully', () => { + // TypeScript would catch this, but testing runtime behavior + expect(shouldRender(initial, { myProp: 1 }, { myState: 1 }, 'invalid' as any)).toBe(false); // Should fall through to customDeep }); }); });