diff --git a/packages/react/react-media-store/src/MediaProvider.tsx b/packages/react/react-media-store/src/MediaProvider.tsx index 445191b6..b7ca9154 100644 --- a/packages/react/react-media-store/src/MediaProvider.tsx +++ b/packages/react/react-media-store/src/MediaProvider.tsx @@ -64,7 +64,61 @@ export const useMediaRef = () => { }; }; -const refEquality = (a: any, b: any) => a === b; +export const refEquality = (a: any, b: any) => a === b; + +const hasOwnProperty = Object.prototype.hasOwnProperty; + +/** + * Slightly modified version of React's shallowEqual, with optimizations for Arrays + * so we may treat them specifically as unequal if they are not a) both arrays + * or b) don't contain the same (shallowly compared) elements. + */ +export const shallowEqual = (objA: any, objB: any): boolean => { + // Using Object.is as a first pass, as it covers a lot of the "simple" cases that are + // more complex than strict equality and is a built-in. For discussion, see, e.g.: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is#description + if (Object.is(objA, objB)) { + return true; + } + + // Since we've done an Object.is() check immediately above, we can safely assume non-objects (or null-valued objects) + // are not equal, so can early bail for those as well. + if ( + typeof objA !== 'object' || + objA === null || + typeof objB !== 'object' || + objB === null + ) { + return false; + } + + if (Array.isArray(objA)) { + // Early "cheap" array compares + if (!Array.isArray(objB) || objA.length !== objB.length) return false; + // Shallow compare for arrays + return objA.some((vVal, i) => objB[i] === vVal); + } + + const keysA = Object.keys(objA); + const keysB = Object.keys(objB); + + if (keysA.length !== keysB.length) { + return false; + } + + // Test for A's keys different from B. + for (let i = 0; i < keysA.length; i++) { + // NOTE: Since we've already guaranteed the keys list lengths are the same, we can safely cast to string here (CJP) + if ( + !hasOwnProperty.call(objB, keysA[i] as string) || + !Object.is(objA[keysA[i] as string], objB[keysA[i] as string]) + ) { + return false; + } + } + + return true; +}; export const useMediaSelector = ( selector: (state: any) => S, diff --git a/packages/react/react-media-store/src/index.tsx b/packages/react/react-media-store/src/index.tsx index dcae8a36..3b201560 100644 --- a/packages/react/react-media-store/src/index.tsx +++ b/packages/react/react-media-store/src/index.tsx @@ -1,14 +1,15 @@ // Re-export everything from MediaProvider - this is the primary implementation -export { - MediaProvider, +export { + MediaProvider, MediaContext, useMediaStore, useMediaDispatch, useMediaRef, - useMediaSelector + useMediaSelector, + shallowEqual, } from './MediaProvider.js'; // @ts-ignore - Placeholder types until media-store exports are updated export type MediaStore = any; export type MediaState = any; -export type MediaStateOwner = any; \ No newline at end of file +export type MediaStateOwner = any;