Skip to content

Commit 7709436

Browse files
robbieculkinnickgros
authored andcommitted
add shallow option. better separation of concerns in tests
1 parent a1b277b commit 7709436

File tree

14 files changed

+454
-250
lines changed

14 files changed

+454
-250
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ should change the heading of the (upcoming) version to include a major version b
2525

2626
## @rjsf/core
2727

28-
- 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.
28+
- 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'`
2929

3030
## @rjsf/utils
3131

3232
- Extended `Registry` interface to include optional `experimental_componentUpdateStrategy` property
33+
- Added `shallowEquals()` utility function for shallow equality comparisons
3334

3435
# 6.0.0-beta.12
3536

packages/core/src/components/Form.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,12 @@ export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
200200
*
201201
* - `'customDeep'`: Uses RJSF's custom deep equality checks via the `deepEquals` utility function,
202202
* which treats all functions as equivalent and provides optimized performance for form data comparisons.
203-
* - `'default'`: Uses React's default component update behavior (shallow comparison).
203+
* - `'shallow'`: Uses shallow comparison of props and state (only compares direct properties). This matches React's PureComponent behavior.
204+
* - `'always'`: Always rerenders when called. This matches React's Component behavior.
204205
*
205206
* @default 'customDeep'
206207
*/
207-
experimental_componentUpdateStrategy?: 'customDeep' | 'default';
208+
experimental_componentUpdateStrategy?: 'customDeep' | 'shallow' | 'always';
208209
/** Optional function that allows for custom merging of `allOf` schemas
209210
*/
210211

@@ -526,14 +527,7 @@ export default class Form<
526527
*/
527528
shouldComponentUpdate(nextProps: FormProps<T, S, F>, nextState: FormState<T, S, F>): boolean {
528529
const { experimental_componentUpdateStrategy = 'customDeep' } = this.props;
529-
530-
if (experimental_componentUpdateStrategy === 'default') {
531-
// Use React's default behavior: always update if state or props change (no shouldComponentUpdate optimization)
532-
return true;
533-
}
534-
535-
// Use custom deep equality checks via shouldRender
536-
return shouldRender(this, nextProps, nextState);
530+
return shouldRender(this, nextProps, nextState, experimental_componentUpdateStrategy);
537531
}
538532
/** Gets the previously raised customValidate errors.
539533
*

packages/core/src/components/fields/SchemaField.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { useCallback, Component, ComponentType } from 'react';
22
import {
33
ADDITIONAL_PROPERTY_FLAG,
4-
deepEquals,
54
descriptionId,
65
ErrorSchema,
76
FieldProps,
@@ -15,6 +14,7 @@ import {
1514
mergeObjects,
1615
Registry,
1716
RJSFSchema,
17+
shouldRender,
1818
StrictRJSFSchema,
1919
TranslatableString,
2020
UI_OPTIONS_KEY,
@@ -345,13 +345,7 @@ class SchemaField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
345345
shouldComponentUpdate(nextProps: Readonly<FieldProps<T, S, F>>) {
346346
const { experimental_componentUpdateStrategy = 'customDeep' } = this.props.registry;
347347

348-
if (experimental_componentUpdateStrategy === 'default') {
349-
// Use React's default behavior: always update if state or props change (no shouldComponentUpdate optimization)
350-
return true;
351-
}
352-
353-
// Use custom deep equality checks
354-
return !deepEquals(this.props, nextProps);
348+
return shouldRender(this, nextProps, this.state, experimental_componentUpdateStrategy);
355349
}
356350

357351
render() {

packages/core/test/Form.test.jsx

Lines changed: 0 additions & 204 deletions
Original file line numberDiff line numberDiff line change
@@ -4972,208 +4972,4 @@ describe('Form omitExtraData and liveOmit', () => {
49724972
expect(errors).to.have.lengthOf(0);
49734973
});
49744974
});
4975-
4976-
describe('experimental_componentUpdateStrategy re-render behavior', () => {
4977-
let renderCount;
4978-
let TestField;
4979-
4980-
beforeEach(() => {
4981-
renderCount = 0;
4982-
4983-
// Create a custom field that tracks renders
4984-
TestField = class extends React.Component {
4985-
render() {
4986-
renderCount++;
4987-
return <input type='text' value={this.props.formData || ''} readOnly />;
4988-
}
4989-
};
4990-
});
4991-
4992-
it('should prevent unnecessary re-renders with customDeep strategy when data unchanged', () => {
4993-
const schema = {
4994-
type: 'object',
4995-
properties: {
4996-
name: { type: 'string' },
4997-
},
4998-
};
4999-
5000-
const formData = { name: 'test' };
5001-
5002-
const { rerender } = createFormComponent({
5003-
schema,
5004-
formData,
5005-
experimental_componentUpdateStrategy: 'customDeep',
5006-
fields: { StringField: TestField },
5007-
});
5008-
5009-
const initialRenderCount = renderCount;
5010-
5011-
// Re-render with same data - should not cause field re-render with customDeep
5012-
rerender({
5013-
schema,
5014-
formData,
5015-
experimental_componentUpdateStrategy: 'customDeep',
5016-
fields: { StringField: TestField },
5017-
});
5018-
5019-
// With customDeep strategy, render count should remain the same for identical data
5020-
expect(renderCount).to.equal(initialRenderCount);
5021-
});
5022-
5023-
it('should allow re-renders with default strategy even when data unchanged', () => {
5024-
const schema = {
5025-
type: 'object',
5026-
properties: {
5027-
name: { type: 'string' },
5028-
},
5029-
};
5030-
5031-
const formData = { name: 'test' };
5032-
5033-
const { rerender } = createFormComponent({
5034-
schema,
5035-
formData,
5036-
experimental_componentUpdateStrategy: 'default',
5037-
fields: { StringField: TestField },
5038-
});
5039-
5040-
const initialRenderCount = renderCount;
5041-
5042-
// Re-render with same data - should cause field re-render with default strategy
5043-
rerender({
5044-
schema,
5045-
formData,
5046-
experimental_componentUpdateStrategy: 'default',
5047-
fields: { StringField: TestField },
5048-
});
5049-
5050-
// With default strategy, render count should increase even for identical data
5051-
expect(renderCount).to.be.greaterThan(initialRenderCount);
5052-
});
5053-
5054-
it('should re-render with customDeep strategy when data actually changes', () => {
5055-
const schema = {
5056-
type: 'object',
5057-
properties: {
5058-
name: { type: 'string' },
5059-
},
5060-
};
5061-
5062-
const initialFormData = { name: 'test' };
5063-
const updatedFormData = { name: 'updated' };
5064-
5065-
const { rerender } = createFormComponent({
5066-
schema,
5067-
formData: initialFormData,
5068-
experimental_componentUpdateStrategy: 'customDeep',
5069-
fields: { StringField: TestField },
5070-
});
5071-
5072-
const initialRenderCount = renderCount;
5073-
5074-
// Re-render with different data - should cause field re-render even with customDeep
5075-
rerender({
5076-
schema,
5077-
formData: updatedFormData,
5078-
experimental_componentUpdateStrategy: 'customDeep',
5079-
fields: { StringField: TestField },
5080-
});
5081-
5082-
// With customDeep strategy and changed data, render count should increase
5083-
expect(renderCount).to.be.greaterThan(initialRenderCount);
5084-
});
5085-
5086-
it('should handle nested object changes with customDeep strategy', () => {
5087-
const schema = {
5088-
type: 'object',
5089-
properties: {
5090-
user: {
5091-
type: 'object',
5092-
properties: {
5093-
name: { type: 'string' },
5094-
age: { type: 'number' },
5095-
},
5096-
},
5097-
},
5098-
};
5099-
5100-
const initialFormData = { user: { name: 'John', age: 25 } };
5101-
5102-
const { rerender } = createFormComponent({
5103-
schema,
5104-
formData: initialFormData,
5105-
experimental_componentUpdateStrategy: 'customDeep',
5106-
fields: { ObjectField: TestField },
5107-
});
5108-
5109-
const initialRenderCount = renderCount;
5110-
5111-
// Re-render with same nested data - should not cause re-render
5112-
rerender({
5113-
schema,
5114-
formData: { user: { name: 'John', age: 25 } }, // Same values, different object reference
5115-
experimental_componentUpdateStrategy: 'customDeep',
5116-
fields: { ObjectField: TestField },
5117-
});
5118-
5119-
// With customDeep strategy, should not re-render for deep-equal data
5120-
expect(renderCount).to.equal(initialRenderCount);
5121-
5122-
// Now change nested data
5123-
rerender({
5124-
schema,
5125-
formData: { user: { name: 'John', age: 26 } }, // Changed age
5126-
experimental_componentUpdateStrategy: 'customDeep',
5127-
fields: { ObjectField: TestField },
5128-
});
5129-
5130-
// Should re-render when nested data actually changes
5131-
expect(renderCount).to.be.greaterThan(initialRenderCount);
5132-
});
5133-
5134-
it('should handle array changes with customDeep strategy', () => {
5135-
const schema = {
5136-
type: 'object',
5137-
properties: {
5138-
items: {
5139-
type: 'array',
5140-
items: { type: 'string' },
5141-
},
5142-
},
5143-
};
5144-
5145-
const initialFormData = { items: ['a', 'b', 'c'] };
5146-
5147-
const { rerender } = createFormComponent({
5148-
schema,
5149-
formData: initialFormData,
5150-
experimental_componentUpdateStrategy: 'customDeep',
5151-
fields: { ArrayField: TestField },
5152-
});
5153-
5154-
const initialRenderCount = renderCount;
5155-
5156-
// Re-render with same array data - should not cause re-render
5157-
rerender({
5158-
schema,
5159-
formData: { items: ['a', 'b', 'c'] }, // Same values, different array reference
5160-
experimental_componentUpdateStrategy: 'customDeep',
5161-
fields: { ArrayField: TestField },
5162-
});
5163-
5164-
// With customDeep strategy, should not re-render for deep-equal arrays
5165-
expect(renderCount).to.equal(initialRenderCount);
5166-
5167-
// Now change array data
5168-
rerender({
5169-
schema,
5170-
formData: { items: ['a', 'b', 'c', 'd'] }, // Added item
5171-
experimental_componentUpdateStrategy: 'customDeep',
5172-
fields: { ArrayField: TestField },
5173-
});
5174-
5175-
// Should re-render when array data actually changes
5176-
expect(renderCount).to.be.greaterThan(initialRenderCount);
5177-
});
5178-
});
51794975
});

0 commit comments

Comments
 (0)