Skip to content

Commit 514ea85

Browse files
feat: improve deepEquals performance (rjsf-team#4292)
* Change fast-deep-equal to fast-equals * Add changelog and change deepEquals depscription * run cs-format * Add JSDocs for isFunctions and customDeepEqual * Update CHANGELOG.md --------- Co-authored-by: Heath C <[email protected]>
1 parent fc1c8c3 commit 514ea85

15 files changed

+89
-42
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ should change the heading of the (upcoming) version to include a major version b
2121
## @rjsf/utils
2222

2323
- Fixes an issue with dependencies computeDefaults to ensure we can get the dependencies defaults [#4271](https://github.com/rjsf-team/react-jsonschema-form/issues/4271)
24+
- Updated `deepEquals()` to use `fast-equals.createCustomEqual()` instead of `lodash.isEqualWith()`, fixing [#4291](https://github.com/rjsf-team/react-jsonschema-form/issues/4291)
25+
- Switched uses of `lodash.isEqual()` to `deepEquals()` in many of the utility functions as well
26+
27+
28+
## @validator-ajv8
29+
30+
- Use `@rjsf/utils` `deepEquals()` instead of `lodash.isEqual()` to improve performance, fixing [#4291](https://github.com/rjsf-team/react-jsonschema-form/issues/4291)
2431

2532
# 5.20.1
2633

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,6 @@
7979
"packages/validator-ajv6",
8080
"packages/validator-ajv8",
8181
"packages/snapshot-tests"
82-
]
82+
],
83+
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
8384
}

packages/utils/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"react": "^16.14.0 || >=17"
3737
},
3838
"dependencies": {
39+
"fast-equals": "^5.0.1",
3940
"json-schema-merge-allof": "^0.8.1",
4041
"jsonpointer": "^5.0.1",
4142
"lodash": "^4.17.21",

packages/utils/src/createSchemaUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ import {
1414
ValidatorType,
1515
} from './types';
1616
import {
17+
getClosestMatchingOption,
1718
getDefaultFormState,
1819
getDisplayLabel,
19-
getClosestMatchingOption,
2020
getFirstMatchingOption,
2121
getMatchingOption,
2222
isFilesArray,

packages/utils/src/deepEquals.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,46 @@
1-
import isEqualWith from 'lodash/isEqualWith';
1+
import { createCustomEqual, State } from 'fast-equals';
22

3-
/** Implements a deep equals using the `lodash.isEqualWith` function, that provides a customized comparator that
3+
/** Check if all parameters are typeof function.
4+
*
5+
* @param a - The first element to check typeof
6+
* @param b - The second element to check typeof
7+
* @returns - if typeof a and b are equal to function return true, otherwise false
8+
*/
9+
function isFunctions(a: any, b: any) {
10+
return typeof a === 'function' && typeof b === 'function';
11+
}
12+
13+
/** Implements a deep equals using the `fast-equal.createCustomEqual` function, that provides a customized comparator that
14+
* assumes all functions in objects are equivalent.
15+
*
16+
* @param a - The first element to compare
17+
* @param b - The second element to compare
18+
* @returns - True if the `a` and `b` are deeply equal, false otherwise
19+
*/
20+
const customDeepEqual = createCustomEqual({
21+
createInternalComparator: (comparator: (a: any, b: any, state: State<any>) => boolean) => {
22+
return (a: any, b: any, _idxA: any, _idxB: any, _parentA: any, _parentB: any, state: State<any>) => {
23+
if (isFunctions(a, b)) {
24+
// Assume all functions are equivalent
25+
// see https://github.com/rjsf-team/react-jsonschema-form/issues/255
26+
return true;
27+
}
28+
29+
return comparator(a, b, state);
30+
};
31+
},
32+
});
33+
34+
/** Implements a deep equals using the `fast-equal.createCustomEqual` function, that provides a customized comparator that
435
* assumes all functions are equivalent.
536
*
637
* @param a - The first element to compare
738
* @param b - The second element to compare
839
* @returns - True if the `a` and `b` are deeply equal, false otherwise
940
*/
1041
export default function deepEquals(a: any, b: any): boolean {
11-
return isEqualWith(a, b, (obj: any, other: any) => {
12-
if (typeof obj === 'function' && typeof other === 'function') {
13-
// Assume all functions are equivalent
14-
// see https://github.com/rjsf-team/react-jsonschema-form/issues/255
15-
return true;
16-
}
17-
return undefined; // fallback to default isEquals behavior
18-
});
42+
if (isFunctions(a, b)) {
43+
return true;
44+
}
45+
return customDeepEqual(a, b);
1946
}

packages/utils/src/enumOptionsDeselectValue.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import isEqual from 'lodash/isEqual';
2-
31
import { EnumOptionsType, RJSFSchema, StrictRJSFSchema } from './types';
42
import enumOptionsValueForIndex from './enumOptionsValueForIndex';
3+
import deepEquals from './deepEquals';
54

65
/** Removes the enum option value at the `valueIndex` from the currently `selected` (list of) value(s). If `selected` is
76
* a list, then that list is updated to remove the enum option value with the `valueIndex` in `allEnumOptions`. If it is
@@ -22,7 +21,7 @@ export default function enumOptionsDeselectValue<S extends StrictRJSFSchema = RJ
2221
): EnumOptionsType<S>['value'] | EnumOptionsType<S>['value'][] | undefined {
2322
const value = enumOptionsValueForIndex<S>(valueIndex, allEnumOptions);
2423
if (Array.isArray(selected)) {
25-
return selected.filter((v) => !isEqual(v, value));
24+
return selected.filter((v) => !deepEquals(v, value));
2625
}
27-
return isEqual(value, selected) ? undefined : selected;
26+
return deepEquals(value, selected) ? undefined : selected;
2827
}

packages/utils/src/enumOptionsIsSelected.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import isEqual from 'lodash/isEqual';
2-
1+
import deepEquals from './deepEquals';
32
import { EnumOptionsType, RJSFSchema, StrictRJSFSchema } from './types';
43

54
/** Determines whether the given `value` is (one of) the `selected` value(s).
@@ -13,7 +12,7 @@ export default function enumOptionsIsSelected<S extends StrictRJSFSchema = RJSFS
1312
selected: EnumOptionsType<S>['value'] | EnumOptionsType<S>['value'][]
1413
) {
1514
if (Array.isArray(selected)) {
16-
return selected.some((sel) => isEqual(sel, value));
15+
return selected.some((sel) => deepEquals(sel, value));
1716
}
18-
return isEqual(selected, value);
17+
return deepEquals(selected, value);
1918
}

packages/utils/src/parser/ParserValidator.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import get from 'lodash/get';
2-
import isEqual from 'lodash/isEqual';
32

43
import { ID_KEY } from '../constants';
54
import hashForSchema from '../hashForSchema';
@@ -15,6 +14,7 @@ import {
1514
ValidationData,
1615
ValidatorType,
1716
} from '../types';
17+
import deepEquals from '../deepEquals';
1818

1919
/** The type of the map of schema hash to schema
2020
*/
@@ -67,7 +67,7 @@ export default class ParserValidator<T = any, S extends StrictRJSFSchema = RJSFS
6767
const existing = this.schemaMap[key];
6868
if (!existing) {
6969
this.schemaMap[key] = identifiedSchema;
70-
} else if (!isEqual(existing, identifiedSchema)) {
70+
} else if (!deepEquals(existing, identifiedSchema)) {
7171
console.error('existing schema:', JSON.stringify(existing, null, 2));
7272
console.error('new schema:', JSON.stringify(identifiedSchema, null, 2));
7373
throw new Error(
@@ -91,7 +91,7 @@ export default class ParserValidator<T = any, S extends StrictRJSFSchema = RJSFS
9191
* @throws - Error when the given `rootSchema` differs from the root schema provided during construction
9292
*/
9393
isValid(schema: S, _formData: T, rootSchema: S): boolean {
94-
if (!isEqual(rootSchema, this.rootSchema)) {
94+
if (!deepEquals(rootSchema, this.rootSchema)) {
9595
throw new Error('Unexpectedly calling isValid() with a rootSchema that differs from the construction rootSchema');
9696
}
9797
this.addSchema(schema, hashForSchema<S>(schema));

packages/utils/src/parser/schemaParser.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import forEach from 'lodash/forEach';
2-
import isEqual from 'lodash/isEqual';
32

43
import { FormContextType, RJSFSchema, StrictRJSFSchema } from '../types';
5-
import { PROPERTIES_KEY, ITEMS_KEY } from '../constants';
4+
import { ITEMS_KEY, PROPERTIES_KEY } from '../constants';
65
import ParserValidator, { SchemaMap } from './ParserValidator';
7-
import { retrieveSchemaInternal, resolveAnyOrOneOfSchemas } from '../schema/retrieveSchema';
6+
import { resolveAnyOrOneOfSchemas, retrieveSchemaInternal } from '../schema/retrieveSchema';
7+
import deepEquals from '../deepEquals';
88

99
/** Recursive function used to parse the given `schema` belonging to the `rootSchema`. The `validator` is used to
1010
* capture the sub-schemas that the `isValid()` function is called with. For each schema returned by the
@@ -24,7 +24,7 @@ function parseSchema<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
2424
) {
2525
const schemas = retrieveSchemaInternal<T, S, F>(validator, schema, rootSchema, undefined, true);
2626
schemas.forEach((schema) => {
27-
const sameSchemaIndex = recurseList.findIndex((item) => isEqual(item, schema));
27+
const sameSchemaIndex = recurseList.findIndex((item) => deepEquals(item, schema));
2828
if (sameSchemaIndex === -1) {
2929
recurseList.push(schema);
3030
const allOptions = resolveAnyOrOneOfSchemas<T, S, F>(validator, schema, rootSchema, true);

0 commit comments

Comments
 (0)