@n0n3br/react-use-deep-compare-effect
Version:
React hook similar to useEffect but with deep comparison for dependencies.
149 lines (148 loc) • 6.59 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.useDeepCompareEffect = exports.areDepsEqualManually = exports.isDeepEqual = void 0;
var react_1 = require("react");
// Internal helper function for deep comparison.
// Handles primitives, objects, arrays, Date, RegExp, and circular references.
function _isDeepEqualInternal(val1, val2, memo // Used to detect and handle circular references
) {
// 1. Identity and primitive comparison (Object.is handles NaN correctly)
if (Object.is(val1, val2)) {
return true;
}
// 2. If types are different or one of them is null (but not both, already handled by Object.is)
// or if they are not objects (meaning they are different primitives, since Object.is failed)
if (typeof val1 !== typeof val2 ||
val1 === null ||
val2 === null ||
(typeof val1 !== "object" && typeof val1 !== "function") || // Ensures both are objects/functions to proceed
(typeof val2 !== "object" && typeof val2 !== "function")) {
return false;
}
// From here, val1 and val2 are non-identical objects or functions.
// Functions are handled by reference by the `areDepsEqualManually` logic,
// but if `isDeepEqual` is called directly with functions, `Object.is` already handled them.
// 3. Cycle detection: if we've already compared val1 and it was paired with val2, it's an equal cycle.
if (memo.has(val1) && memo.get(val1) === val2) {
return true;
}
// Add the pair to the memo before recursion.
// We use `object` as the key type for WeakMap, as only objects can be keys.
if (typeof val1 === "object" && typeof val2 === "object") {
memo.set(val1, val2);
}
// 4. Specific object types
if (val1 instanceof Date && val2 instanceof Date) {
return val1.getTime() === val2.getTime();
}
if (val1 instanceof RegExp && val2 instanceof RegExp) {
return val1.source === val2.source && val1.flags === val2.flags;
}
// If one is Date/RegExp and the other isn't (and they are not identical by reference, already checked)
if (val1 instanceof Date !== val2 instanceof Date ||
val1 instanceof RegExp !== val2 instanceof RegExp) {
return false;
}
// 5. Arrays
if (Array.isArray(val1) && Array.isArray(val2)) {
if (val1.length !== val2.length)
return false;
for (var i = 0; i < val1.length; i++) {
if (!_isDeepEqualInternal(val1[i], val2[i], memo))
return false;
}
return true;
}
// If one is an array and the other isn't (and not identical by reference)
if (Array.isArray(val1) !== Array.isArray(val2))
return false;
// 6. Objects (plain objects or other complex objects not handled above)
// Check if both are generic objects (not arrays, not null)
var isVal1Object = typeof val1 === "object" && val1 !== null;
var isVal2Object = typeof val2 === "object" && val2 !== null;
if (isVal1Object && isVal2Object) {
var keys1 = Object.keys(val1);
var keys2 = Object.keys(val2);
if (keys1.length !== keys2.length)
return false;
for (var _i = 0, keys1_1 = keys1; _i < keys1_1.length; _i++) {
var key = keys1_1[_i];
if (!Object.prototype.hasOwnProperty.call(val2, key))
return false;
if (!_isDeepEqualInternal(val1[key], val2[key], memo)) {
return false;
}
}
return true;
}
// If it reached here, types are different or not deeply comparable as expected
// (e.g., a function and an object, or types not explicitly covered)
return false;
}
/**
* Performs a deep comparison between two values.
* @param val1 The first value.
* @param val2 The second value.
* @returns `true` if the values are deeply equal, `false` otherwise.
*/
function isDeepEqual(val1, val2) {
return _isDeepEqualInternal(val1, val2, new WeakMap());
}
exports.isDeepEqual = isDeepEqual;
/**
* Compares two dependency arrays, using deep comparison for objects/arrays
* and reference comparison for functions.
*/
function areDepsEqualManually(// Exporting for testing purposes
prevDeps, nextDeps) {
if (prevDeps === undefined) {
// First render or no previous dependencies
return false;
}
if (prevDeps.length !== nextDeps.length) {
return false;
}
for (var i = 0; i < prevDeps.length; i++) {
var prevDep = prevDeps[i];
var nextDep = nextDeps[i];
// Rule: Functions are compared by reference
if (typeof prevDep === "function" && typeof nextDep === "function") {
if (prevDep !== nextDep) {
return false;
}
continue; // Move to the next dependency
}
// For other types, use deep comparison
if (!isDeepEqual(prevDep, nextDep)) {
return false;
}
}
return true;
}
exports.areDepsEqualManually = areDepsEqualManually;
/**
* React hook that behaves like `useEffect`, but performs a deep comparison
* of dependencies instead of a shallow reference comparison.
*
* @param callback Function to be executed when dependencies change.
* Can return a cleanup function.
* @param dependencies Dependency array. The callback is executed if any
* dependency changes deeply (except functions, which are
* compared by reference).
*/
function useDeepCompareEffect(callback, dependencies) {
var prevDependenciesRef = (0, react_1.useRef)(undefined);
var signalRef = (0, react_1.useRef)(0); // A value that changes to trigger useEffect
// Check if dependencies have actually changed through deep comparison.
// This check is done here, outside useEffect, to decide whether to update
// prevDependenciesRef and signalRef.
if (!areDepsEqualManually(prevDependenciesRef.current, dependencies)) {
prevDependenciesRef.current = dependencies; // Store the new dependencies
signalRef.current++; // Change the "signal" to trigger the effect
}
// The native useEffect now only depends on `signalRef.current`.
// It will be executed when `signalRef.current` changes,
// which only happens when our deep comparison logic indicates a real change.
(0, react_1.useEffect)(callback, [signalRef.current]);
}
exports.useDeepCompareEffect = useDeepCompareEffect;