diff --git a/CHANGELOG.md b/CHANGELOG.md index 453e899d1e..b014249c06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ should change the heading of the (upcoming) version to include a major version b - Fix default value population when switching between options in `MultiSchemaField` [#4375](https://github.com/rjsf-team/react-jsonschema-form/pull/4375). Fixes [#4367](https://github.com/rjsf-team/react-jsonschema-form/issues/4367) +## @rjsf/utils + +- Short-circuit `File` and `Date` constructor access in isObject to optimize performance in scenarios where `globalThis` is a `Proxy` that incurs overhead for each class constructor access ([#4413](https://github.com/rjsf-team/react-jsonschema-form/pull/4413)). Fixes [#4409](https://github.com/rjsf-team/react-jsonschema-form/issues/4409) + ## @rjsf/validator-ajv8 - Fixed issue where `ui:title` in anyOf/oneOf is not shown in error messages. Fixes [#4368](https://github.com/rjsf-team/react-jsonschema-form/issues/4368) diff --git a/packages/utils/src/isObject.ts b/packages/utils/src/isObject.ts index f45832a721..e830849234 100644 --- a/packages/utils/src/isObject.ts +++ b/packages/utils/src/isObject.ts @@ -1,15 +1,22 @@ -/** Determines whether a `thing` is an object for the purposes of RSJF. In this case, `thing` is an object if it has +/** Determines whether a `thing` is an object for the purposes of RJSF. In this case, `thing` is an object if it has * the type `object` but is NOT null, an array or a File. * * @param thing - The thing to check to see whether it is an object * @returns - True if it is a non-null, non-array, non-File object */ export default function isObject(thing: any) { - if (typeof File !== 'undefined' && thing instanceof File) { + if (typeof thing !== 'object' || thing === null) { return false; } - if (typeof Date !== 'undefined' && thing instanceof Date) { + // lastModified is guaranteed to be a number on a File instance + // as per https://w3c.github.io/FileAPI/#dfn-lastModified + if (typeof thing.lastModified === 'number' && typeof File !== 'undefined' && thing instanceof File) { return false; } - return typeof thing === 'object' && thing !== null && !Array.isArray(thing); + // getMonth is guaranteed to be a method on a Date instance + // as per https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-date.prototype.getmonth + if (typeof thing.getMonth === 'function' && typeof Date !== 'undefined' && thing instanceof Date) { + return false; + } + return !Array.isArray(thing); } diff --git a/packages/utils/test/isObject.test.ts b/packages/utils/test/isObject.test.ts index b1e6197cea..6926ccd6a2 100644 --- a/packages/utils/test/isObject.test.ts +++ b/packages/utils/test/isObject.test.ts @@ -26,4 +26,38 @@ describe('isObject()', () => { expect(isObject(object)).toBe(true); }); }); + describe('without accessing File and Date classes', () => { + const NativeFile = File; + const NativeDate = Date; + + beforeEach(() => { + Object.defineProperty(global, 'File', { + get() { + throw new Error('File should not have been accessed'); + }, + }); + Object.defineProperty(global, 'Date', { + get() { + throw new Error('Date should not have been accessed'); + }, + }); + }); + + afterEach(() => { + Object.defineProperty(global, 'File', NativeFile); + Object.defineProperty(global, 'Date', NativeDate); + }); + + it('returns false when a non-object is provided', () => { + NON_OBJECTS.forEach((nonObject: string | number | boolean | null | undefined) => { + expect(isObject(nonObject)).toBe(false); + }); + }); + + it('returns true when an object is provided', () => { + OBJECTS.forEach((object: any) => { + expect(isObject(object)).toBe(true); + }); + }); + }); });