Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: switch lodash.isEqual to fast-equals.deepEqual to improve performance #4438

Merged
merged 5 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ should change the heading of the (upcoming) version to include a major version b
- Fixed issue with assigning default values to formData with deeply nested required properties, fixing [#4399](https://github.com/rjsf-team/react-jsonschema-form/issues/4399)
- Fixed issue error message will not be cleared after the controlled Form formData is changed. Fixes [#4426](https://github.com/rjsf-team/react-jsonschema-form/issues/4426)
- Fix for AJV [$data](https://ajv.js.org/guide/combining-schemas.html#data-reference) reference in const property in schema treated as default/const value. The issue is mentioned in [#4361](https://github.com/rjsf-team/react-jsonschema-form/issues/4361).
- Switched uses of `lodash.isEqual()` to `@rjsf/utils.deepEquals`.

## @rjsf/validator-ajv8

- Partially fixed issue where dependency errors do not show `title` or `ui:title`. This fix only applicable if we use an ajv-i18n localizer. Ref. [#4402](https://github.com/rjsf-team/react-jsonschema-form/issues/4402).
- Switched uses of `lodash.isEqual()` to `@rjsf/utils.deepEquals` at precompiledValidator.

# 5.23.2

Expand Down
7 changes: 3 additions & 4 deletions packages/utils/src/enumOptionsDeselectValue.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import isEqual from 'lodash/isEqual';

import { EnumOptionsType, RJSFSchema, StrictRJSFSchema } from './types';
import enumOptionsValueForIndex from './enumOptionsValueForIndex';
import deepEquals from './deepEquals';

/** Removes the enum option value at the `valueIndex` from the currently `selected` (list of) value(s). If `selected` is
* a list, then that list is updated to remove the enum option value with the `valueIndex` in `allEnumOptions`. If it is
Expand All @@ -22,7 +21,7 @@ export default function enumOptionsDeselectValue<S extends StrictRJSFSchema = RJ
): EnumOptionsType<S>['value'] | EnumOptionsType<S>['value'][] | undefined {
const value = enumOptionsValueForIndex<S>(valueIndex, allEnumOptions);
if (Array.isArray(selected)) {
return selected.filter((v) => !isEqual(v, value));
return selected.filter((v) => !deepEquals(v, value));
}
return isEqual(value, selected) ? undefined : selected;
return deepEquals(value, selected) ? undefined : selected;
}
7 changes: 3 additions & 4 deletions packages/utils/src/enumOptionsIsSelected.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import isEqual from 'lodash/isEqual';

import { EnumOptionsType, RJSFSchema, StrictRJSFSchema } from './types';
import deepEquals from './deepEquals';

/** Determines whether the given `value` is (one of) the `selected` value(s).
*
Expand All @@ -13,7 +12,7 @@ export default function enumOptionsIsSelected<S extends StrictRJSFSchema = RJSFS
selected: EnumOptionsType<S>['value'] | EnumOptionsType<S>['value'][]
) {
if (Array.isArray(selected)) {
return selected.some((sel) => isEqual(sel, value));
return selected.some((sel) => deepEquals(sel, value));
}
return isEqual(selected, value);
return deepEquals(selected, value);
}
6 changes: 3 additions & 3 deletions packages/utils/src/parser/ParserValidator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';

import { ID_KEY } from '../constants';
import hashForSchema from '../hashForSchema';
Expand All @@ -15,6 +14,7 @@ import {
ValidationData,
ValidatorType,
} from '../types';
import deepEquals from '../deepEquals';

/** The type of the map of schema hash to schema
*/
Expand Down Expand Up @@ -67,7 +67,7 @@ export default class ParserValidator<T = any, S extends StrictRJSFSchema = RJSFS
const existing = this.schemaMap[key];
if (!existing) {
this.schemaMap[key] = identifiedSchema;
} else if (!isEqual(existing, identifiedSchema)) {
} else if (!deepEquals(existing, identifiedSchema)) {
console.error('existing schema:', JSON.stringify(existing, null, 2));
console.error('new schema:', JSON.stringify(identifiedSchema, null, 2));
throw new Error(
Expand All @@ -91,7 +91,7 @@ export default class ParserValidator<T = any, S extends StrictRJSFSchema = RJSFS
* @throws - Error when the given `rootSchema` differs from the root schema provided during construction
*/
isValid(schema: S, _formData: T, rootSchema: S): boolean {
if (!isEqual(rootSchema, this.rootSchema)) {
if (!deepEquals(rootSchema, this.rootSchema)) {
throw new Error('Unexpectedly calling isValid() with a rootSchema that differs from the construction rootSchema');
}
this.addSchema(schema, hashForSchema<S>(schema));
Expand Down
8 changes: 4 additions & 4 deletions packages/utils/src/parser/schemaParser.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import forEach from 'lodash/forEach';
import isEqual from 'lodash/isEqual';

import { FormContextType, RJSFSchema, StrictRJSFSchema } from '../types';
import { PROPERTIES_KEY, ITEMS_KEY } from '../constants';
import { ITEMS_KEY, PROPERTIES_KEY } from '../constants';
import ParserValidator, { SchemaMap } from './ParserValidator';
import { retrieveSchemaInternal, resolveAnyOrOneOfSchemas } from '../schema/retrieveSchema';
import { resolveAnyOrOneOfSchemas, retrieveSchemaInternal } from '../schema/retrieveSchema';
import deepEquals from '../deepEquals';

/** Recursive function used to parse the given `schema` belonging to the `rootSchema`. The `validator` is used to
* capture the sub-schemas that the `isValid()` function is called with. For each schema returned by the
Expand All @@ -24,7 +24,7 @@ function parseSchema<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
) {
const schemas = retrieveSchemaInternal<T, S, F>(validator, schema, rootSchema, undefined, true);
schemas.forEach((schema) => {
const sameSchemaIndex = recurseList.findIndex((item) => isEqual(item, schema));
const sameSchemaIndex = recurseList.findIndex((item) => deepEquals(item, schema));
if (sameSchemaIndex === -1) {
recurseList.push(schema);
const allOptions = resolveAnyOrOneOfSchemas<T, S, F>(validator, schema, rootSchema, true);
Expand Down
18 changes: 10 additions & 8 deletions packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { JSONSchema7Object } from 'json-schema';

import {
ALL_OF_KEY,
ANY_OF_KEY,
CONST_KEY,
DEFAULT_KEY,
DEPENDENCIES_KEY,
PROPERTIES_KEY,
ONE_OF_KEY,
PROPERTIES_KEY,
REF_KEY,
ALL_OF_KEY,
} from '../constants';
import findSchemaDefinition from '../findSchemaDefinition';
import getClosestMatchingOption from './getClosestMatchingOption';
Expand All @@ -33,10 +34,9 @@ import isMultiSelect from './isMultiSelect';
import isSelect from './isSelect';
import retrieveSchema, { resolveDependencies } from './retrieveSchema';
import isConstant from '../isConstant';
import { JSONSchema7Object } from 'json-schema';
import constIsAjvDataReference from '../constIsAjvDataReference';
import isEqual from 'lodash/isEqual';
import optionsList from '../optionsList';
import deepEquals from '../deepEquals';

const PRIMITIVE_TYPES = ['string', 'number', 'integer', 'boolean', 'null'];

Expand Down Expand Up @@ -130,8 +130,7 @@ function maybeAddDefaultToObject<T = any>(
if (!isEmpty(computedDefault)) {
obj[key] = computedDefault;
}
}
// Else store computedDefault if it's a non-empty object(e.g. not {}) and satisfies certain conditions
} // Else store computedDefault if it's a non-empty object(e.g. not {}) and satisfies certain conditions
// Condition 1: If computedDefault is not empty or if the key is a required field
// Condition 2: If the parent object is required or emptyObjectFields is not 'populateRequiredDefaults'
else if (
Expand Down Expand Up @@ -276,7 +275,10 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
experimental_dfsb_to_compute?.constAsDefaults === 'skipOneOf'
) {
// If we are in a oneOf of a primitive type, then we want to pass constAsDefaults as 'never' for the recursion
experimental_dfsb_to_compute = { ...experimental_dfsb_to_compute, constAsDefaults: 'never' };
experimental_dfsb_to_compute = {
...experimental_dfsb_to_compute,
constAsDefaults: 'never',
};
}
schemaToCompute = oneOf![
getClosestMatchingOption<T, S, F>(
Expand Down Expand Up @@ -382,7 +384,7 @@ export function ensureFormDataMatchingSchema<
let validFormData: T | T[] | undefined = formData;
if (isSelectField) {
const getOptionsList = optionsList(schema);
const isValid = getOptionsList?.some((option) => isEqual(option.value, formData));
const isValid = getOptionsList?.some((option) => deepEquals(option.value, formData));
validFormData = isValid ? formData : undefined;
}

Expand Down
13 changes: 8 additions & 5 deletions packages/utils/src/schema/retrieveSchema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
import set from 'lodash/set';
import times from 'lodash/times';
import transform from 'lodash/transform';
Expand All @@ -15,10 +14,10 @@ import {
ANY_OF_KEY,
DEPENDENCIES_KEY,
IF_KEY,
ITEMS_KEY,
ONE_OF_KEY,
REF_KEY,
PROPERTIES_KEY,
ITEMS_KEY,
REF_KEY,
} from '../constants';
import findSchemaDefinition, { splitKeyElementFromObject } from '../findSchemaDefinition';
import getDiscriminatorFieldFromSchema from '../getDiscriminatorFieldFromSchema';
Expand All @@ -34,6 +33,7 @@ import {
ValidatorType,
} from '../types';
import getFirstMatchingOption from './getFirstMatchingOption';
import deepEquals from '../deepEquals';

/** Retrieves an expanded schema that has had all of its conditions, additional properties, references and dependencies
* resolved and merged into the `schema` given a `validator`, `rootSchema` and `rawFormData` that is used to do the
Expand Down Expand Up @@ -256,7 +256,10 @@ export function resolveSchema<T = any, S extends StrictRJSFSchema = RJSFSchema,
)
);
const allPermutations = getAllPermutationsOfXxxOf<S>(allOfSchemaElements);
return allPermutations.map((permutation) => ({ ...schema, allOf: permutation }));
return allPermutations.map((permutation) => ({
...schema,
allOf: permutation,
}));
}
// No $ref or dependencies or allOf attribute was found, returning the original schema.
return [schema];
Expand Down Expand Up @@ -356,7 +359,7 @@ export function resolveAllReferences<S extends StrictRJSFSchema = RJSFSchema>(
};
}

return isEqual(schema, resolvedSchema) ? schema : resolvedSchema;
return deepEquals(schema, resolvedSchema) ? schema : resolvedSchema;
}

/** Creates new 'properties' items for each key in the `formData`
Expand Down
4 changes: 2 additions & 2 deletions packages/utils/src/schema/toIdSchema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';

import { ALL_OF_KEY, DEPENDENCIES_KEY, ID_KEY, ITEMS_KEY, PROPERTIES_KEY, REF_KEY } from '../constants';
import isObject from '../isObject';
Expand All @@ -14,6 +13,7 @@ import {
} from '../types';
import retrieveSchema from './retrieveSchema';
import getSchemaType from '../getSchemaType';
import deepEquals from '../deepEquals';

/** An internal helper that generates an `IdSchema` object for the `schema`, recursively with protection against
* infinite recursion
Expand Down Expand Up @@ -42,7 +42,7 @@ function toIdSchemaInternal<T = any, S extends StrictRJSFSchema = RJSFSchema, F
): IdSchema<T> {
if (REF_KEY in schema || DEPENDENCIES_KEY in schema || ALL_OF_KEY in schema) {
const _schema = retrieveSchema<T, S, F>(validator, schema, rootSchema, formData, experimental_customMergeAllOf);
const sameSchemaIndex = _recurseList.findIndex((item) => isEqual(item, _schema));
const sameSchemaIndex = _recurseList.findIndex((item) => deepEquals(item, _schema));
if (sameSchemaIndex === -1) {
return toIdSchemaInternal<T, S, F>(
validator,
Expand Down
6 changes: 3 additions & 3 deletions packages/utils/src/schema/toPathSchema.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
import set from 'lodash/set';

import {
ADDITIONAL_PROPERTIES_KEY,
ALL_OF_KEY,
ANY_OF_KEY,
ADDITIONAL_PROPERTIES_KEY,
DEPENDENCIES_KEY,
ITEMS_KEY,
NAME_KEY,
Expand All @@ -26,6 +25,7 @@ import {
} from '../types';
import getClosestMatchingOption from './getClosestMatchingOption';
import retrieveSchema from './retrieveSchema';
import deepEquals from '../deepEquals';

/** An internal helper that generates an `PathSchema` object for the `schema`, recursively with protection against
* infinite recursion
Expand All @@ -50,7 +50,7 @@ function toPathSchemaInternal<T = any, S extends StrictRJSFSchema = RJSFSchema,
): PathSchema<T> {
if (REF_KEY in schema || DEPENDENCIES_KEY in schema || ALL_OF_KEY in schema) {
const _schema = retrieveSchema<T, S, F>(validator, schema, rootSchema, formData, experimental_customMergeAllOf);
const sameSchemaIndex = _recurseList.findIndex((item) => isEqual(item, _schema));
const sameSchemaIndex = _recurseList.findIndex((item) => deepEquals(item, _schema));
if (sameSchemaIndex === -1) {
return toPathSchemaInternal<T, S, F>(
validator,
Expand Down
9 changes: 5 additions & 4 deletions packages/validator-ajv8/src/precompiledValidator.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { ErrorObject } from 'ajv';
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';

import {
CustomValidator,
deepEquals,
ErrorSchema,
ErrorTransformer,
FormContextType,
hashForSchema,
ID_KEY,
JUNK_OPTION_ID,
retrieveSchema,
RJSFSchema,
StrictRJSFSchema,
toErrorList,
UiSchema,
ValidationData,
ValidatorType,
retrieveSchema,
} from '@rjsf/utils';

import { CompiledValidateFunction, Localizer, ValidatorFunctions } from './types';
Expand Down Expand Up @@ -92,10 +93,10 @@ export default class AJV8PrecompiledValidator<
* @param [formData] - The form data to validate if any
*/
ensureSameRootSchema(schema: S, formData?: T) {
if (!isEqual(schema, this.rootSchema)) {
if (!deepEquals(schema, this.rootSchema)) {
// Resolve the root schema with the passed in form data since that may affect the resolution
const resolvedRootSchema = retrieveSchema(this, this.rootSchema, this.rootSchema, formData);
if (!isEqual(schema, resolvedRootSchema)) {
if (!deepEquals(schema, resolvedRootSchema)) {
throw new Error(
'The schema associated with the precompiled validator differs from the rootSchema provided for validation'
);
Expand Down
Loading