Skip to content
Open
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
77 changes: 77 additions & 0 deletions packages/core/src/bundle/helpers/deepEqual/deepEqual.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const isObject = (value) => typeof value === 'object' && value !== null;
const isPlainObject = (value) => {
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
};
/**
* @name deepEqual
* @description - Performs a deep comparison between two values to determine if they are equivalent
* @category Helpers
* @usage low
*
* @param {any} a The first value to compare
* @param {any} b The second value to compare
* @returns {boolean} `true` if the two values are deeply equal, `false` otherwise
*
* @warning - Set members and Map keys are compared by reference, Map values are compared deeply. Objects without own enumerable string keys are compared by reference unless they are plain objects.
*
* @example
* deepEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } }); // true
*/
export const deepEqual = (a, b) => {
const seen = new WeakMap();
const mark = (x, y) => {
const visited = seen.get(x);
if (visited) {
if (visited.has(y)) return true;
visited.add(y);
return false;
}
seen.set(x, new WeakSet([y]));
return false;
};
const compare = (x, y) => {
if (Object.is(x, y)) return true;
if (!isObject(x) || !isObject(y)) return false;
if (Object.getPrototypeOf(x) !== Object.getPrototypeOf(y)) return false;
if (mark(x, y)) return true;
if (x instanceof Date) return y instanceof Date && Object.is(x.getTime(), y.getTime());
if (x instanceof RegExp)
return y instanceof RegExp && x.source === y.source && x.flags === y.flags;
if (x instanceof Error)
return y instanceof Error && x.name === y.name && x.message === y.message;
if (Array.isArray(x)) {
if (!Array.isArray(y) || x.length !== y.length) return false;
for (let index = 0; index < x.length; index += 1) {
if (!compare(x[index], y[index])) return false;
}
return true;
}
if (x instanceof Set) {
if (!(y instanceof Set) || x.size !== y.size) return false;
for (const value of x) {
if (!y.has(value)) return false;
}
return true;
}
if (x instanceof Map) {
if (!(y instanceof Map) || x.size !== y.size) return false;
for (const [key, value] of x) {
if (!y.has(key) || !compare(value, y.get(key))) return false;
}
return true;
}
const xo = x;
const yo = y;
const keysX = [...Object.keys(xo), ...Object.getOwnPropertySymbols(xo)];
const keysY = [...Object.keys(yo), ...Object.getOwnPropertySymbols(yo)];
if (keysX.length !== keysY.length) return false;
if (!Object.keys(xo).length && !isPlainObject(x)) return false;
for (const key of keysX) {
if (!Object.hasOwn(yo, key)) return false;
if (!compare(xo[key], yo[key])) return false;
}
return true;
};
return compare(a, b);
};
2 changes: 2 additions & 0 deletions packages/core/src/bundle/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ export * from './createEventEmitter/createEventEmitter';
export * from './createReactiveContext/createReactiveContext';
export * from './createSharedHook/createSharedHook';
export * from './createStore/createStore';
export * from './deepEqual/deepEqual';
export * from './makeDestructurable/makeDestructurable';
export * from './shallowEqual/shallowEqual';
61 changes: 61 additions & 0 deletions packages/core/src/bundle/helpers/shallowEqual/shallowEqual.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const isObject = (value) => typeof value === 'object' && value !== null;
const isPlainObject = (value) => {
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
};
/**
* @name shallowEqual
* @description - Performs a shallow comparison between two values to determine if they are equivalent
* @category Helpers
* @usage low
*
* @param {any} a The first value to compare
* @param {any} b The second value to compare
* @returns {boolean} `true` if the two values are shallowly equal, `false` otherwise
*
* @warning - Set members and Map keys are compared by reference. Objects without own enumerable string keys are compared by reference unless they are plain objects.
*
* @example
* shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); // true
*/
export const shallowEqual = (a, b) => {
if (Object.is(a, b)) return true;
if (!isObject(a) || !isObject(b)) return false;
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
if (a instanceof Date) return b instanceof Date && Object.is(a.getTime(), b.getTime());
if (a instanceof RegExp)
return b instanceof RegExp && a.source === b.source && a.flags === b.flags;
if (a instanceof Error) return b instanceof Error && a.name === b.name && a.message === b.message;
if (Array.isArray(a)) {
if (!Array.isArray(b) || a.length !== b.length) return false;
for (let index = 0; index < a.length; index += 1) {
if (!Object.is(a[index], b[index])) return false;
}
return true;
}
if (a instanceof Set) {
if (!(b instanceof Set) || a.size !== b.size) return false;
for (const value of a) {
if (!b.has(value)) return false;
}
return true;
}
if (a instanceof Map) {
if (!(b instanceof Map) || a.size !== b.size) return false;
for (const [key, value] of a) {
if (!b.has(key) || !Object.is(value, b.get(key))) return false;
}
return true;
}
const ao = a;
const bo = b;
const keysA = [...Object.keys(ao), ...Object.getOwnPropertySymbols(ao)];
const keysB = [...Object.keys(bo), ...Object.getOwnPropertySymbols(bo)];
if (keysA.length !== keysB.length) return false;
if (!Object.keys(ao).length && !isPlainObject(a)) return false;
for (const key of keysA) {
if (!Object.hasOwn(bo, key)) return false;
if (!Object.is(ao[key], bo[key])) return false;
}
return true;
};
2 changes: 2 additions & 0 deletions packages/core/src/bundle/hooks/lifecycle.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from './useAsyncEffect/useAsyncEffect';
export * from './useCustomCompareEffect/useCustomCompareEffect';
export * from './useDeepEffect/useDeepEffect';
export * from './useDidUpdate/useDidUpdate';
export * from './useIsFirstRender/useIsFirstRender';
export * from './useIsomorphicLayoutEffect/useIsomorphicLayoutEffect';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useEffect, useRef } from 'react';
/**
* @name useCustomCompareEffect
* @description - Hook that triggers the effect callback when the comparator reports the dependencies as changed
* @category Lifecycle
* @usage low
*
* @param {EffectCallback} effect The effect callback
* @param {DependencyList} deps The dependencies list for the effect
* @param {(deps: DependencyList, prevDeps: DependencyList) => boolean} comparator The function that returns `true` when the dependencies are equal
*
* @example
* useCustomCompareEffect(() => console.log("effect"), [user], ([user], [prevUser]) => user.id === prevUser.id);
*/
export const useCustomCompareEffect = (effect, deps, comparator) => {
const depsRef = useRef(undefined);
const signalRef = useRef(0);
if (!deps || !depsRef.current || !comparator(deps, depsRef.current)) signalRef.current += 1;
depsRef.current = deps;
useEffect(effect, [signalRef.current]);
};
19 changes: 19 additions & 0 deletions packages/core/src/bundle/hooks/useDeepEffect/useDeepEffect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { deepEqual } from '@/helpers/deepEqual/deepEqual';
import { useCustomCompareEffect } from '../useCustomCompareEffect/useCustomCompareEffect';
/**
* @name useDeepEffect
* @description - Hook that executes an effect only when dependencies change deeply
* @category Lifecycle
* @usage low
*
* @param {EffectCallback} effect The effect callback
* @param {DependencyList} [deps] The dependencies list for the effect
*
* @warning - Use `useCustomCompareEffect` with your own comparator when the comparison rules do not fit
*
* @example
* useDeepEffect(() => console.log("effect"), [user]);
*/
export const useDeepEffect = (effect, deps) => {
useCustomCompareEffect(effect, deps, deepEqual);
};
Original file line number Diff line number Diff line change
@@ -1,37 +1,21 @@
import { useEffect, useRef } from 'react';
export const deepEqual = (a, b) => {
if (a === b) return true;
if (a == null || b == null) return a === b;
if (typeof a !== typeof b) return false;
if (typeof a !== 'object') return a === b;
if (Array.isArray(a) !== Array.isArray(b)) return false;
if (Array.isArray(a))
return a.length === b.length && a.every((value, index) => deepEqual(value, b[index]));
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!keysB.includes(key)) return false;
if (!deepEqual(a[key], b[key])) return false;
}
return true;
};
import { shallowEqual } from '@/helpers/shallowEqual/shallowEqual';
import { useCustomCompareEffect } from '../useCustomCompareEffect/useCustomCompareEffect';
const shallowEqualDeps = (deps, prevDeps) =>
deps.length === prevDeps.length && deps.every((dep, index) => shallowEqual(dep, prevDeps[index]));
/**
* @name useShallowEffect
* @description - Hook that executes an effect only when dependencies change shallowly or deeply
* @description - Hook that executes an effect only when dependencies change shallowly
* @category Lifecycle
* @usage low
*
* @param {EffectCallback} effect The effect callback
* @param {DependencyList} [deps] The dependencies list for the effect
*
* @warning - Use `useCustomCompareEffect` with your own comparator when the comparison rules do not fit
*
* @example
* useShallowEffect(() => console.log("effect"), [user]);
*/
export const useShallowEffect = (effect, deps) => {
const depsRef = useRef(deps);
if (!depsRef.current || !deepEqual(deps, depsRef.current)) {
depsRef.current = deps;
}
useEffect(effect, depsRef.current);
useCustomCompareEffect(effect, deps, shallowEqualDeps);
};
Loading